Compare commits

..

522 Commits

Author SHA1 Message Date
Tanner Bennett 700c50af5d Bump version 2020-04-06 17:37:59 -05:00
Tanner Bennett b38cca06b1 Fix ProtocolMember instance field
The ProtocolMember instance field was not being populated in runtime exports
2020-04-06 17:32:43 -05:00
Tanner Bennett 6429573918 Rename some classes with excessively long names 2020-04-06 17:32:43 -05:00
Tanner Bennett f77f5ccdc9 Push straight to single instances in heap explorer
Take the user straight to the explorer itself if there is only one instance of the selected class
2020-04-06 17:32:43 -05:00
Tanner Bennett 7aeddcdb2c Add search bar to view controllers list 2020-04-06 17:32:11 -05:00
Tanner Bennett a25ef87a51 Make new JSON viewer and System Log behavior opt-in 2020-04-06 17:32:11 -05:00
Tanner Bennett fbeb1beca0 Namespace some category filenames 2020-04-06 17:32:11 -05:00
Tanner Bennett 059bde9711 Fix crash in flex_all* methods 2020-04-06 17:32:11 -05:00
Tanner Bennett 2ca563f570 Bug fix: iPad support for FLEXAlert action sheets 2020-04-06 17:32:11 -05:00
Tanner Bennett 88c7ca9373 Add option to disable property/ivar previews 2020-03-31 12:16:57 -05:00
Tanner Bennett 83486641aa Add < iOS 13 support to example project 2020-03-30 16:28:19 -05:00
Tanner Bennett 6bd0c87881 Fix import statement to work without modules 2020-03-30 16:28:19 -05:00
Tanner Bennett 1a64da70c9 Add FLEXRuntimeExporter
Allows you to export the contents of a bundle's objc metadata as an SQLite database

fix

fix
2020-03-30 16:28:19 -05:00
Tanner Bennett 87ea2bb147 FLEXRuntimeClient additions
- Move initializeWebKitLegacy to FLEXRuntimeClient
- Add -copySafeClassList and -copyProtocolList
2020-03-27 19:59:32 -05:00
Tanner Bennett d9e9be53d8 Various database browser related upgrades
- All database managers automatically open and close connections to the underlying database
- Allow getting last result and last rowid from FLEXSQLiteDatabaseManager
- Execute statements with arguments with FLEXSQLiteDatabaseManager
2020-03-27 19:59:32 -05:00
Tanner Bennett 142f037497 Runtime wrapper upgrades
- Make sure every object has an `imageName` property
- Expose more fine-grained metadata through FLEXProtocol
- Migrate flex_all* methods to top-level functions that the methods call into
2020-03-27 19:59:32 -05:00
Tanner Bennett 6cdb626d78 Various bug fixes 2020-03-27 19:59:32 -05:00
Tanner Bennett 6e81029b8b Show HTTP status code in commit screen error 2020-03-27 19:16:09 -05:00
Tanner Bennett 1c7048e710 Dispatch JSON viewer registration to main queue 2020-03-25 15:12:34 -05:00
Tanner Bennett 23c7cfbe6e Update README.md 2020-03-24 15:17:59 -05:00
Tanner Bennett b45750eb1b Update .gitignore 2020-03-24 14:53:32 -05:00
Tanner Bennett 550c9c1120 Add new Swift Example project
The project is built with FLEXFilteringTableViewController and utilizes FLEXObjectExplorerFactory to demonstrate how to use them in your own projects. It also shows how to properly import use FLEX in Swift projects.
2020-03-24 14:53:32 -05:00
Tanner Bennett d5dfb23cf2 Fix possible shortcuts bug 2020-03-24 14:53:32 -05:00
Tanner Bennett 90ee4f8f38 Fix behavior of M key shortcut 2020-03-24 14:33:01 -05:00
Tanner Bennett c037e703a6 Fix runtime browser crashing when searching for *.*.
Also add type annotations to several arrays
2020-03-24 14:20:36 -05:00
Tanner Bennett 6bf746d4aa Initialize WebKitLegacy when exploring all classes 2020-03-24 14:20:36 -05:00
Tanner Bennett ca005fb4d0 Make FLEXClassIsSafe return NO for unknown root classes 2020-03-24 14:19:00 -05:00
Tanner Bennett 5f2bac9c2f Fix network search adding all new entries to search results 2020-03-24 14:18:52 -05:00
Tanner Bennett dc0e7dc7d3 Tap navigation bar to reveal toolbar if hidden 2020-03-24 14:18:52 -05:00
Tanner Bennett 740daab55b Add network host blacklisting in UI
Make networkRequestHostBlacklist a mutable array
2020-03-24 14:18:52 -05:00
Tanner Bennett 1953089653 Move "copy curl" button to the toolbar 2020-03-24 11:50:53 -05:00
Tanner Bennett 25e0af042e Fix network screen not updating in the background 2020-03-24 11:50:53 -05:00
Tanner Bennett 0265334976 Truncate object descriptions to 10000 characters
Also fix description sizing bug
2020-03-24 11:50:53 -05:00
Chaoshuai Lü 6aa9ec9ec1 Make some functions static to avoid warnings (#394) 2020-03-24 10:40:36 -05:00
Tanner Bennett 459aa9b6f5 Move FLEXManager+Private.h 2020-03-18 16:30:46 -05:00
Tanner Bennett 06655dde6a More type encoding parser tests + bug fixes 2020-03-18 16:30:46 -05:00
Tanner Bennett 5c153b0c89 Work around AFNetworking breaking network observer 2020-03-18 16:30:46 -05:00
Tanner Bennett 29dd970ea7 Whoops 2020-03-16 17:25:54 -05:00
Tanner Bennett f6701f8ec9 Move network monitor "Clear All" button to the toolbar 2020-03-16 17:25:54 -05:00
Tanner Bennett 7f119ba0cc Add option to reveal overridden methods
- Publish certain preference changes with NSNotificationCenter so as to update all screens observing these changes
- Observe these preferences in appropriate screens
2020-03-16 14:41:46 -05:00
Tanner Bennett e7e5115fc0 Clean up FLEXNetworkObserver.m 2020-03-16 13:15:13 -05:00
Tanner Bennett 0e0bd5a890 Don't add a "Done" button unless we're being presented 2020-03-16 13:15:13 -05:00
Tanner Bennett 9cc2470901 Add some missing nullability specifiers
Improve the Swift experience a little bit
2020-03-16 13:15:13 -05:00
Tanner Bennett b07da3e11d Update copyright dates 2020-03-16 13:15:13 -05:00
Tanner Bennett 36e0e9fb1e Bump version, expose more public headers 2020-03-16 13:15:12 -05:00
Tanner Bennett 0e85162c11 Fix or silence various warnings
warnings
2020-03-16 13:15:12 -05:00
Tanner Bennett 7329fd9272 Expose toolbar as a public property on FLEXManager 2020-03-11 16:57:43 -05:00
Tanner Bennett 30fb8f077a Forgot to format FLEXResources.m 2020-03-10 18:21:14 -05:00
Tanner Bennett f2f66489d1 Use properties where applicable 2020-03-10 18:16:27 -05:00
Tanner Bennett 3cb1366966 Replace +array / +string / +set … calls with +new 2020-03-10 18:16:27 -05:00
Tanner Bennett 907b315601 Namespace files in RuntimeBrowser/ 2020-03-10 18:16:26 -05:00
Tanner Bennett 877a1db87b Add ability to hide property-backing ivars+methods 2020-03-10 16:38:33 -05:00
Tanner Bennett 5b6b50bf6a Fix various gesture bugs in the FLEX toolbar
My bad y'all
2020-03-10 16:38:33 -05:00
Tanner Bennett 6c8fbbeaa8 Fix system log in simulator 2020-03-10 16:38:33 -05:00
Tanner Bennett da67902cf5 Add automatic filtering table view controller
Also add FLEXMutableListSection which wraps the collection content section in a way that makes displaying a simple list of content straightforward.

Adopt additions in appropriate view controllers.
2020-03-10 16:38:33 -05:00
Tanner Bennett 8533689ca7 Clean up FLEXNetworkRecorder
Network history will prompt to enable the monitor on tap
2020-03-10 15:23:44 -05:00
Tanner Bennett 8f85b22866 Recent icon change 2020-03-09 12:20:37 -05:00
Tanner Bennett fa8a4d61ea New icons 2020-03-09 12:20:37 -05:00
Tanner Bennett 89010395de Search upgrades
- Add ability to search object lists (instances, references, subclasses)
- Fix broken search for collection exploreres
2020-03-09 12:20:37 -05:00
Tanner Bennett 5b969b6438 Refactor FLEXClassShortcuts, add "List Subclasses" 2020-03-09 12:20:37 -05:00
Tanner Bennett 2dee6901f7 Refactor NSObject+Reflection 2020-03-09 12:20:37 -05:00
Tanner Bennett fced419509 Bug fixes / code cleanup
Also make image preview use checker background
2020-03-09 12:20:37 -05:00
Tanner Bennett 09ff0482df iOS 9-10 fixes 2020-03-09 12:20:37 -05:00
Tanner Bennett d9986d879b Fix broken os_log_shim_enabled substrate hook
Not sure how I ever thought it was working the way it was before in the first place…
2020-03-09 12:20:37 -05:00
Les Melnychuk 15d7d07809 Add ability to run SQL queries 2020-03-03 12:41:45 -06:00
Tanner Bennett a556ece626 Tidy up database browser 2020-03-02 17:51:08 -06:00
Tanner Bennett 35ce037288 No longer override UIMenuController items 2020-03-02 17:51:08 -06:00
Tanner Bennett a32b4074f8 Fix pasting not working in runtime browser 2020-02-28 16:12:21 -06:00
Tanner Bennett c3da7e10f7 Improve runtime browser accessory view behavior
- Buttons are right-aligned when they don't fill the toolbar
- Keyboard now has period key, giving more space to other buttons in the toolbar
- Suggestions for classes and bundles are now added to the toolbar
- When nothing has been typed, the default suggestion is the bundle for the current app
- Make the colors closer to the real keyboard colors
2020-02-28 16:12:21 -06:00
Tanner Bennett 135c8d05c1 Carousel header works on all supported iOS versions 2020-02-28 16:12:21 -06:00
Tanner Bennett 8afd1a1975 Selecting a Mach-O file will push an array of classes 2020-02-27 11:18:16 -06:00
Tanner Bennett dcdd638719 Don't call methods on objects in instances screen 2020-02-27 11:18:16 -06:00
Tanner Bennett c661d491a5 Use SF Mono / Menlo for log font 2020-02-27 11:18:16 -06:00
Tanner Bennett 82f104d682 Runtime browser improvements 2020-02-24 18:03:07 -06:00
Tanner Bennett f9e42aed74 Support NSDecimalNumber setters 2020-02-24 18:03:07 -06:00
Tanner Bennett c8343500af Tab-related fixes 2020-02-24 18:03:07 -06:00
Tanner Bennett 5a96ed6af5 Add a swipe-to-dismiss gesture for fullscreen pages
Swipe down on the navigation bar to dismiss fullscreen view controllers. Also, every view controller on the navigation stack gets a "done" button.
2020-02-24 18:03:07 -06:00
Tanner Bennett 84cdc6a8e4 Fix priorities of swipe / pan gestures in explorer 2020-02-24 18:03:07 -06:00
Tanner Bennett 2b6ccb23e4 Exclude swipe gestures from the carousel 2020-02-24 18:03:07 -06:00
Tanner Bennett be02b89d9b Add swipe gestures to select a more shallow or deeper view 2020-02-24 18:03:07 -06:00
Tanner Bennett b4f07a0f92 Remove unnecessary refresh control in object explorer
Most update as you scroll, there is little benefit to even having a refresh control. It also inhibits the drag-to-dismiss iOS 13 sheet gesture.
2020-02-24 18:03:07 -06:00
Tanner Bennett 2093913b17 Add missing accessory button to scenes in windows screen 2020-02-24 18:03:07 -06:00
Tanner Bennett 7705dac42b Add shortcuts for various foundation classes 2020-02-24 18:03:07 -06:00
Tanner Bennett d23f01dd87 Move UIView(Controller) runtime property additions
… to FLEXShortcutsFactory+Defaults.m
2020-02-24 18:03:07 -06:00
Tanner Bennett ec2b73ceda Add some objc runtime nullability safeguards 2020-02-24 18:03:07 -06:00
Tanner Bennett b8f226ce45 Add more singletons to the globals screen 2020-02-24 18:03:07 -06:00
Tanner Bennett b232ddb075 Fix shortcut registration logic
Fix shortcuts
2020-02-24 18:03:07 -06:00
Tanner Bennett 87a821903d Misc FLEX*Utility changes
Also don't load NSProxy categories if testing
2020-02-24 18:03:07 -06:00
Tanner Bennett 9645811baa Don't call other methods on target in performSelector: 2020-02-24 18:03:07 -06:00
Tanner Bennett 2e2a550b1f Nullability fixes for runtime functions 2020-02-24 18:03:07 -06:00
Tanner Bennett 98dff514e8 Moooooore type encoding parser fixes
- Case: NSMethodSignature does not support unions
- Case: NSMethodSignature passes incompete pointer types to NSGetSizeAndAlignment which throws an exception
2020-02-24 18:03:07 -06:00
Tanner Bennett 9332a87d98 FLEXTypeEncodingParser: Improve malformed array detection 2020-02-24 18:03:07 -06:00
Tanner Bennett d031dab174 Actually disable os_log for jailbroken devices
os_log cannot be disabled on-device without a trampoline-style function hook. Fishhook just rebinds lazy symbols, which isn't working on-device for some reason. This commit makes use of Substrate's MSHookFunction to do this, if it is available.
2020-02-24 18:03:07 -06:00
Tanner Bennett 7ddb46ff9f Update parser: NSMethodSignature doesn't support unions 2020-02-19 16:10:33 -06:00
Tanner Bennett 4a4a08df16 Optimize FLEXTypeEncodingParser 2020-02-19 16:10:33 -06:00
Tanner Bennett 4830daeab3 Fix exceptions caught by NSMethodSignature
FLEXTypeEncodingParser worked as originally intended, but getting it working helped me realize something else: NSMethodSignature will pass structure member pointer types to NSGetSizeAndAlignment and catch any exceptions it throws.

So now FLEXTypeEncoding must parse and "clean" unsupported pointer types in method signatures to avoid those exceptions, where previously we didn't care about opinter types at all.

+[FLEXTypeEncoding methodTypeEncodingSupported:] becomes +[FLEXTypeEncoding methodTypeEncodingSupported:cleaned:] where `cleaned` is an NSString out param which stores the "cleaned" method signature (or the input signature if nothing needed cleaning). This is then passed to NSMethodSignature.

This commit also fixes a few other bugs, like arithmetic errors on when calculating the size of "v" and union size calculation.
2020-02-19 16:10:33 -06:00
Tanner Bennett dd05a6652c Remove needless refresh on viewWillAppear: 2020-02-19 16:10:33 -06:00
Tanner Bennett e78947260d Refactor FLEXToolbar*, add a "recent" button
Recent button presents most recently active tab, if any
2020-02-17 22:38:18 -06:00
Tanner Bennett 5dd856070e Undo comment in app delegate 2020-02-17 15:26:12 -06:00
Tanner Bennett 2e868eba39 Add more descriptive titles to editor screens 2020-02-13 17:21:20 -06:00
Tanner Bennett 9ba80a53cf Add fishhook, disable OS log — close #372
iOS 10 and its associated SDK deprecated *ASL and replaced it with *os_log. This change is widely considered unfavorable and made it extremely tedious for FLEX to intercept log messages reliably.

@Ram4096 has brought to my attention that the os_log functionality is actually just a shim which is conditionally enabled based on what SDK version your binary links with. With a little reverse engineering, I was able to hook the function that tells `NSLog` (well, `CFLogv`) whether os_log should be used or not. This commit uses fishhook to hook `os_log_shim_enabled` to always return `NO` so that the old ASL library is used instead.

Prior to this commit we had code in place to conditionally intercept messages from os_log or ASL based on the iOS version. These checks are not semantically correct since ASL would still be used on iOS 10+ if the binary was built with the iOS 9 SDK. For now, this doesn't matter going forward since we are going to always use ASL, but it might be worth updating the check to instead check for the linked SDK version instead of the OS version.

- *ASL: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/asl.3.html
- *os_log: https://developer.apple.com/documentation/os/logging?language=objc
2020-02-13 15:46:59 -06:00
Tanner Bennett 1780968f50 Shorten system log class names 2020-02-13 15:31:38 -06:00
Tanner Bennett 9abd9cc933 Reorganize Utility folder 2020-02-13 15:26:36 -06:00
Tanner Bennett 18f20fbf3b Long press on hierarchy item to show VCs at tap 2020-02-13 12:53:45 -06:00
Tanner Bennett 3e6d18dd8c Allow exploring objects from window/scene screen 2020-02-13 12:52:51 -06:00
Tanner Bennett 99d2ddd001 Adopt FLEXTypeEncodingParser
Debugging will be soooo much less frustrating now that we aren't throwing exceptions all over the place.

Also, we no longer load pre-registered shortcuts in testing environments. This may come back to bite me in the butt if it turns out the XCTest class exists in UI tests or something.
2020-02-11 19:08:55 -06:00
Tanner Bennett 1647f7ab9f Add FLEXTypeEncodingParser 2020-02-11 19:05:45 -06:00
Tanner Bennett 5ae64e17e6 Fix crash in network screen while searching 2020-02-11 16:57:38 -06:00
Tanner Bennett c5d4e959cd SwiftObject-related fixes 2020-02-11 16:57:38 -06:00
Tanner Bennett c056a29375 Add window and scene managemenet screen
Also fix potential bugs in FLEXExplorerViewController
2020-02-11 16:57:38 -06:00
Tanner Bennett 02409d8051 Add basic support for bookmarking objects 2020-02-11 16:57:38 -06:00
Tanner Bennett 805434239a Fix bugs in collection and defaults content sections
FLEXCollectionContentSection and FLEXDefaultsContentSection
2020-02-11 16:57:38 -06:00
Tanner Bennett 179c968443 Fix bugs in FLEXNetworkHistoryTableViewController
Also, rename it to FLEXNetworkMITMViewController
2020-02-11 16:57:38 -06:00
Tanner Bennett cfc68017e0 Change OS_ACTIVITY_MODE to OS_ACTIVITY_DT_MODE
Silences annoying internal crap in the console
2020-02-11 16:57:38 -06:00
Tanner Bennett 2300d68321 Add basic support for tabs
Other changes:
- Editor/caller view controllers use a toolbar for the call/set button now
- FLEXNavigationController adds the Done button to it's root view controller instead of FLEXExplorerViewController
- FLEXExplorerViewController now overrides presentViewController: and dismissViewControllerAnimated: to toggle its window's key status instead of using new methods to do it
- Adds a 't' simulator shortcut to quickly present an explorer screen for testing
2020-02-11 16:57:38 -06:00
Tanner Bennett 873e79d2c0 Change how simulator shortcuts present tools 2020-02-06 14:21:22 -06:00
Tanner Bennett 17e74a7b02 Fix crash in FLEXInstancesTableViewController
Also remove "table" from the name
2020-02-06 14:21:22 -06:00
Tanner Bennett 795cff68fd Clean up formatting in various files
- Braces on same line
- Comments and method calls curbed to be near or under 100 characters per line
2020-02-06 14:21:22 -06:00
Tanner Bennett 162bf48b5e FLEXGlobalsTableViewController → FLEXGlobalsView…r 2020-02-06 14:21:22 -06:00
Tanner Bennett 239afdbd7c Refactor keyWindow-related logic
First, we give FLEXUtility some methods to grab the app's keyWindow (and the active UIWindowScene on iOS 13).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- Renamed: FLEXFieldEditorViewController → FLEXVariableEditorViewController
- Renamed: FLEXMutableFieldEditorViewController → FLEXFieldEditorViewController
- Collapsed FLEXPropertyEditorViewController and FLEXIvarEditorViewController into their parent class, FLEXFieldEditorViewController
- Property/ivar editor now takes a FLEXProperty/FLEXIvar
- Property/ivar editor initializer is failable based on editability of property/ivar
- FLEXMethodCallingViewController now takes a FLEXMethod
- Argument input views will now generally allow editing of a nil value
2020-01-17 17:15:22 -06:00
Tanner Bennett 3d05e4fb6a Shortcuts and sections
These classes will be the direct data sources for explorer view controllers, and runtime metadata sections will pull their data from a `FLEXObjectExplorer` instance tied to an object explorer view controller
2020-01-17 17:15:22 -06:00
Tanner Bennett ba1de91f85 Add FLEXObjectExplorer
This class will become the data source for object explorer view controllers.
2020-01-17 17:15:22 -06:00
Tanner Bennett 1e9379dc02 Runtime utilities upgrades
An objc runtime wrapper derived from NSExceptional/MirrorKit has been added and will come to replace a few small classes and many of the methods in the FLEXRuntimeUtility and FLEXUtility classes.
2020-01-17 17:15:22 -06:00
Tanner Bennett 965419bd58 Refactor FLEXTableViewCell 2020-01-17 17:15:22 -06:00
Tanner Bennett b21fbabd67 Alert user if app delegate does not provide -window 2020-01-17 17:15:22 -06:00
Tanner Bennett 31446c01be globalsEntryRowAction: should take precedence in FLEXGlobalsEntry 2020-01-17 17:15:22 -06:00
Tanner Bennett 81a3336053 NSArray+Functional additions 2020-01-17 13:54:25 -06:00
Tanner Bennett 8367342b25 Bump podspec version 2019-12-26 17:30:03 -06:00
Tanner Bennett 2df073a792 Keep search bar active between screens
Not sure why I ever added this code. Possibly to ignore a glitch on older versions of iOS? It works fine now on iOS 13, though.
2019-12-26 17:16:46 -06:00
Tanner Bennett 8236fc97cc Clean up cell reuse identifiers 2019-12-20 14:08:28 -06:00
Tanner Bennett 0364de36bd Add FLEXPluralString in FLEXUtility 2019-12-19 18:46:07 -06:00
Tanner Bennett 12195eb879 NSArray+Functional 2019-12-19 18:46:07 -06:00
Tanner Bennett acdc46c43f Add -subviews and -superview @properties to UIView 2019-12-19 18:46:07 -06:00
Tanner Bennett 52eed1b6f9 Add convenience init to some view controllers
Clean up libraries view controller
2019-12-19 18:46:07 -06:00
Tanner Bennett a91d1de9ad Change some private API accessors to use KVC
`performSelector:` can leak. The `KVC` methods are much safer and more reliable. If you pass it the exact method name, that will be called first, assuming there isn't a method with the same name but prefixed with `get`.
2019-12-19 18:46:07 -06:00
Tanner Bennett 492d2e49fe Fix bug in potentiallyUnwrapBoxedPointer: 2019-12-19 18:46:07 -06:00
Tanner Bennett 49bc439000 Delete unused class 2019-12-15 22:14:27 -06:00
Tanner Bennett 8c919cc26c Fix #359 a little better 2019-12-10 13:56:01 -06:00
Tanner Bennett 8e86ffccd6 Bump podspec version, add warning flag to podspec
Our podspec needs -Wno-unsupported-availability-guard for now
2019-12-09 15:34:20 -06:00
Tanner Bennett 228de102e7 Fix #359
Custom UIMenu callouts were not not appearing because the UITextEffectsWindow has a lower level than the FLEX window by default. Until a text field is activated, it would sit below the FLEX window, and custom callouts (copy, copy address) would not appear.
2019-12-09 14:54:41 -06:00
Tanner Bennett 37b5d1be2a Swipe to delete keychain items 2019-12-09 10:48:28 -06:00
renwei.chen ef8f866330 Bug fixes
- Fix root view controller global not working
- Fix crash in keychain viewer when viewing NSData
2019-12-09 10:48:28 -06:00
Tanner Bennett e862b81734 Update README.md 2019-12-05 17:26:10 -06:00
Tanner Bennett a3f66b3f87 Fix scope carousel not working properly on iOS 13 2019-12-02 23:13:38 -06:00
Tanner Bennett 3e12ad9887 Add explicit header titles to several screens
Some screens used the ugly "plain" table view or used a group table view with no title. Both look awful. This commit updates the cookies, keychain, libraries, and live objects screens to use grouped table views with a descriptive section header, like "5 cookies" or "123 of 456 objects, 30 MB"

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

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

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

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

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

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

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

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

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

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

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

* [DarkMode] Load icons as templates

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

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

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

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

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

* [DarkMode] Use dynamic color providers for iOS13+

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

* [DarkMode] Remove last reference for scrollViewGrayColor

* [DarkMode] Rename backgroundColor* to systemBackgroundColor*

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

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

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

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

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

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

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

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

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

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

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

With:
 - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
2018-05-03 12:18:55 -07:00
Bharat Mediratta d30c642707 Adjust FLEX_AT_LEAST_IOS11_SDK to work with -Wexpansion-to-defined 2018-05-03 12:10:22 -07:00
Bharat Mediratta f463e2b43e Xcode 9.3 automated fixes 2018-05-03 12:07:46 -07:00
Shoaib Meenai ed49a4fc89 Use FOUNDATION_EXTERN for global variable
Global variables that can be accessed from both C (or Objective-C) and
C++ (or Objective-C++) source files should be marked `extern "C"` when
in C++ mode (which is what `FOUNDATION_EXTERN` does), to ensure
consistent access across languages.
2018-04-03 21:40:04 -07:00
Chaoshuai Lu b897250fde Fix generics 2018-03-22 16:41:55 -07:00
Greg Heo 06709a5afe Reorder to match order in Installation section 2018-01-23 23:07:47 -08:00
Greg Heo 6eee9e6080 Refactor Installation section 2018-01-23 23:07:47 -08:00
Greg Heo c50e6e51c5 Add Swift example 2018-01-23 23:07:47 -08:00
Marcus Ficner d1e9248695 Fix typo 2018-01-23 23:05:10 -08:00
Chaoshuai Lu a535f10d0c Add generics to foundation collection classes 2018-01-23 23:04:33 -08:00
Chaoshuai Lu 009aa5e3f9 Add iPhone X safe area handling to layout the toolbar correctly 2017-12-17 23:12:41 -08:00
Chaoshuai Lu 675f03fc71 Make Travis use xcodebuild (and xcpretty) instead of (deprecated) xctool 2017-12-17 22:48:15 -08:00
Ryan Olson 99eccdf4c3 Merge pull request #178 from tikoyesayan/master
Fix crash in FLEXRuntimeUtility
2017-08-27 17:22:00 -07:00
Ryan Olson 29afa5e80f Merge pull request #177 from CodeLife2012/networkbug
Should keep the same completion logic
2017-08-27 16:53:20 -07:00
Ryan Olson bf26bc6539 Fix formatting from 62ef95f 2017-08-27 15:56:08 -07:00
Ryan Olson f7b40646e2 Fix network search bar showing over pushed request detail pages 2017-08-27 15:52:22 -07:00
Ryan Olson 84c1fb159b Merge pull request #194 from adysart/xcode9
Xcode9
2017-08-27 15:29:13 -07:00
Aidan Dysart 62ef95ff93 satisfy the -Wstrict-prototypes clang warning 2017-08-08 11:01:10 -07:00
Aidan Dysart 731b729db7 Upgrade project and scheme for Xcode 9, enable default warnings 2017-08-08 11:00:48 -07:00
Tigran Yesayan a1c464d1a7 Add missing free function in testMethodListForClass: method in unit test 2017-07-11 12:48:05 +04:00
Tigran Yesayan eb2ecbf9b3 Added unit test to check the new way of getting method components 2017-07-09 14:56:56 +04:00
Tigran Yesayan 16fab66f7b Fix crash in FLEXRuntimeUtility 2017-07-09 14:52:41 +04:00
Ryan Olson b3e70ac491 Merge pull request #189 from NSExceptional/refactor-toolbar
Make FLEXExplorerToolbar more extensible
2017-07-05 08:58:24 -07:00
Tanner Bennett d5177bb049 Make FLEXExplorerToolbar more extensible
Exposes `FLEXExplorerToolbar.toolbarItems` as a public API so that others can more easily, safely, and reliably add their own items to the FLEX toolbar.
2017-07-03 23:50:46 -05:00
Ryan Olson bb0faeb3cf Merge pull request #184 from revolter/hotfix/code-style
Fix code style
2017-07-03 18:10:17 -07:00
Ryan Olson 761feef3c0 Merge pull request #190 from showbie/request-error-in-red
Highlight request name in red if status code is a 4xx or 5xx
2017-07-03 17:47:00 -07:00
Colin Humber 352bae03ea Highlight request name in red if status code is a 4xx or 5xx 2017-06-29 12:58:01 -06:00
Ryan Olson b0085cae7d Merge pull request #187 from nicked/patch-1
Fixed keyboard shortcuts in Xcode 9 simulator
2017-06-19 17:41:44 -07:00
nicked b2f93f1752 Fixed keyboard shortcuts in Xcode 9 simulator
Pressing Cmd+S in the Simulator normally just takes a screenshot, but in Xcode 9, it also triggers the default "s" keyboard shortcut which enables the FLEX Select tool. This is because the keyboard flags is ANDed with a logical NOT of UIKeyModifierShift instead of the bitwise inverse
2017-06-14 18:03:26 +02:00
Ryan Olson 354510f2c4 Merge pull request #185 from showbie/host-blacklist
Add ability to filter out noisy network requests from being recorded
2017-05-31 22:23:26 -07:00
Colin Humber b8c6175193 Added configuration to filter out network requests for particular hosts from being recorded 2017-05-31 14:18:46 -06:00
Iulian Onofrei d409b110f5 Fix code style 2017-05-23 10:31:02 +03:00
Ryan Olson 5c73220158 Merge pull request #182 from NSExceptional/fix-heap
Fix #119
2017-05-21 22:33:26 -07:00
Tanner Bennett 397721e7ea Fix #119 2017-05-21 18:20:42 -05:00
Ryan Olson 5714275bcd Merge pull request #176 from NSIRLConnection/master
Fix #175
2017-05-21 15:27:56 -07:00
Ryan Olson 833c584e41 Merge pull request #180 from revolter/feature/database-password
Add support for SQLCipher protected databases
2017-05-21 15:22:31 -07:00
Ryan Olson 49b24487c5 Merge pull request #181 from revolter/feature/toolbar-position
Add toolbar position persistence
2017-05-21 15:17:49 -07:00
Iulian Onofrei 6d4eb01a07 Add toolbar position persistence 2017-05-10 17:33:42 +03:00
Iulian Onofrei a752203ff9 Add support for SQLCipher protected databases 2017-05-10 17:12:24 +03:00
Karl Peng c69427613d should keep the same completion logic 2017-03-24 17:29:08 +08:00
Michael 5d75a83568 Fix #175 by checking if the objects respond to compare: 2017-03-09 14:06:47 -05:00
Ryan Olson 7642a0632d Merge pull request #160 from defagos/carthage-instructions-image
Add image for Carthage FLEX exclusion setup
2017-01-15 12:07:48 -08:00
Ryan Olson 841054a713 Merge pull request #161 from unixzii/master
Add a convenient action button in FLEXImagePreviewViewController
2017-01-15 12:05:37 -08:00
杨弘宇 49f368fd63 replace Copy button with Action button in FLEXImagePreviewViewController's navigation item 2016-11-08 20:58:57 +08:00
Samuel Défago 1dc99250c8 Add image for Carthage FLEX exclusion setup 2016-11-07 09:09:19 +01:00
Ryan Olson 00edccf326 Merge pull request #159 from defagos/carthage-instructions
Add Carthage instructions for FLEX exclusion from release builds
2016-11-04 16:54:32 -07:00
Samuel Défago 7f3af90645 Add Carthage instructions for FLEX exclusion from release builds 2016-11-04 07:43:09 +01:00
Ryan Olson 1a030f06cd Merge pull request #157 from froody/size
[FLEX] Add size to heap objects view
2016-10-20 19:48:28 -07:00
Tom Birch 0477858bed [FLEX] Add size to heap objects view
Change-Id: I757714b2570d400886959caa72e731ef65a925b5
2016-10-19 19:55:19 -07:00
Ryan Olson 000e061d00 Merge pull request #152 from rtyu128/rtyu128-FLEXNetworkObserver-Fixed
Fix #150.
2016-10-10 08:16:59 -07:00
Anchor 224978b31b Fixed #150.
Use @selector(URLSession:dataTask:didBecomeDownloadTask:) instead of @selector(URLSession:dataTask:didBecomeDownloadTask:delegate:) in method search.
2016-10-08 10:58:56 +08:00
Ryan Olson 7fd133f13b Bump version in podspec 2016-10-01 15:46:38 -07:00
Ryan Olson e40054ba1a Merge pull request #142 from shepting/patch-1
Fix typo pruged -> pruned
2016-09-30 16:42:58 -07:00
Ryan Olson 1761734447 Merge pull request #149 from c0diq/xcode8
Add Xcode8 support
2016-09-30 16:41:54 -07:00
Ryan Olson f23ee3cd95 Merge pull request #151 from Blankdlh/master
Fix method swizzling for NSURLSessionDelegate
2016-09-30 16:38:55 -07:00
kunka e455ac0c7d Fix swizzling for URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: and URLSession:dataTask:didReceiveResponse:completionHandler: 2016-09-29 18:10:31 +08:00
Sylvain Rebaud 94f68c6dfe Add Xcode8 support 2016-09-26 15:15:36 -07:00
Steven Hepting a22f022014 Fix typo. 2016-08-15 08:17:07 -07:00
Ryan Olson 22e7edb698 Bring back the memory reader for heap enumeration
Some malloc replacements don't provide a default reader
2016-08-09 08:55:54 -07:00
Ryan Olson 52fcda53c5 Merge pull request #138 from iampgzp/master
Added curl logger for debugging request
2016-08-01 10:15:44 -07:00
Ji Pei be6c5d0e43 Removed curl log in recordRequestWillBeSentWithRequestID to avoid too much logging spew 2016-08-01 12:49:32 -04:00
Ryan Olson 6ed0037f50 Merge pull request #134 from ThePantsThief/master
Search for a specific class
2016-07-29 15:35:49 -07:00
Ryan Olson 81e3a5ff47 Merge pull request #132 from revolter/patch-1
Update FLEXRuntimeUtility.m
2016-07-29 15:27:32 -07:00
Ji Pei 2a6e28c9d0 Added curl logger for debugging request 2016-07-27 16:52:47 -04:00
ThePantsThief 9e928b0b09 Fix incorrect strcmp check 2016-07-22 12:49:17 -05:00
Tanner Bennett d5deaad628 Search for a specific class
Added a way to jump to a specific class from the System Libraries tab,
since it is sometimes difficult to find a class if you don’t know which
bundle it’s in.

Also fixed a few other things that bugged me, like manually getting the
last path component of a string instead of using the lastPathComponent
property…
2016-07-06 15:04:39 -05:00
Iulian Onofrei 232ae8a6fd Update FLEXRuntimeUtility.m
Fix typos
2016-06-21 10:46:57 +03:00
Ryan Olson c766e5d94a Merge pull request #128 from Flipboard/timoliver-realm-path-fix
Replaced deprecated 'path' property in Realm configuration
2016-05-20 06:15:30 -07:00
Tim Oliver 5f27a2304b Replaced 'path' property with 'fileURL' in Realm configuration 2016-05-20 15:21:24 +08:00
Ryan Olson 0cb0f44f18 Update version in .podspec 2016-02-28 22:18:38 -08:00
Ryan Olson d1c1aa0a26 Fix xcodebuild error from incorrect method resolution 2016-02-28 22:15:07 -08:00
Ryan Olson 832957f621 Merge pull request #114 from SoXeon/master
fix crash when object malloc_size no greater than zero
2016-02-28 21:52:30 -08:00
Ryan Olson 24985ac984 Fix crash on calling _keyCode on events from the simulator keyboard 2016-02-28 21:43:39 -08:00
dazi.dp 0b652c2f2a add malloc.h 2016-02-25 22:31:47 +08:00
dazi.dp 98d83bb438 fix crash when object malloc_size no greater than zero 2016-02-25 22:20:22 +08:00
Tim Oliver e13717b056 Merge pull request #112 from TimOliver/timoliver-realm-null-fix
Added handling for nil Realm property values.
2016-02-23 22:12:57 +08:00
Tim Oliver 552e687b9c Added handling for nil Realm property values 2016-02-23 22:08:51 +08:00
Ryan Olson fb29421644 Merge pull request #110 from TimOliver/patch-1
Updated database features in README.
2016-02-22 22:08:27 -08:00
Tim Oliver f5d930bd58 Updated database features in README. 2016-02-22 02:10:15 +08:00
Ryan Olson 26af4ef476 Merge pull request #105 from TimOliver/timoliver-realm-integration
Added Realm Database File Introspection Alongside SQLite
2016-02-21 09:39:55 -08:00
Ryan Olson ac8940da26 Merge pull request #107 from nin9tyfour/master
*Added the ability to determine a UIView's view controller without selection the view controller's view delegate.
2016-02-16 20:24:36 -08:00
Tim Oliver f597152a62 Cleaned up example project. 2016-02-17 11:35:39 +08:00
Tim Oliver 58f94f108c Made protocol required. 2016-02-17 11:34:54 +08:00
Tim Oliver a5e0bbd50e Added better extension-checking logic. 2016-02-17 11:34:36 +08:00
Tim Oliver ac273fbfc9 Added test Realm file and classes to the sample app. 2016-02-17 11:20:17 +08:00
Tim Oliver 548fd03bd5 Made Realm integration a dynamic runtime check. 2016-02-17 11:19:17 +08:00
nin9tyfour b564c25d2a *Added the ability to determine a UIView's view controller without selection the view controller's view delegate. 2016-02-16 15:29:52 +11:00
Ryan Olson bd821dc553 Restrict simulator force touch support to iOS 9+ 2016-02-13 13:44:00 -08:00
Ryan Olson e46a33417b Merge pull request #106 from ReadmeCritic/master
Correct the spelling of CocoaPods in README
2016-02-13 13:36:37 -08:00
ReadmeCritic a3419a841f Correct the spelling of CocoaPods in README 2016-02-11 15:46:43 -08:00
Tim Oliver cafc1ba0bd Reset UICatalog project. 2016-02-09 14:16:54 +08:00
Tim Oliver 0112c097d9 Updated Realm optional macros and cleaned project changes. 2016-02-09 14:10:52 +08:00
Tim Oliver 507d03fd90 Cleaned up project file, and added test Realm file. 2016-02-07 16:47:31 +08:00
Tim Oliver b6453ac360 Made references to Realm build optional. 2016-02-06 15:36:49 +08:00
Tim Oliver b693ceb20e Cleaned up protocol logic. 2016-02-06 14:42:19 +08:00
Ryan Olson 31e81a616d Merge pull request #104 from Flipboard/dzc-add-contributing
Add contributing stub
2016-01-30 15:13:42 -08:00
David Creemer 928f60b56f Add contributing stub 2016-01-28 13:19:10 -08:00
Tim Oliver 12a1900d75 Abstracted database parser logic and added framework for Realm parser. 2016-01-28 12:50:39 +08:00
Ryan Olson c72b6f7e5b Merge pull request #102 from pra85/patch-1
Update license year range to 2016
2016-01-18 11:43:12 -08:00
Prayag Verma 3cf9f72dcb Update license year range to 2016 2016-01-18 14:12:03 +05:30
Ryan Olson 9b1318e975 Project structure cleanup 2016-01-17 20:52:34 -08:00
Ryan Olson d3d0f04c23 Heap scan cleanup 2016-01-17 19:54:38 -08:00
Ryan Olson b606d04944 Fix keyboard shortcuts firing when typing into an alert view 2016-01-11 13:27:15 -08:00
Ryan Olson 7afd50d241 Remove asl hacks needed for iOS 7 2016-01-08 15:01:42 -08:00
Ryan Olson 94f06d5ff8 Fix heap search/enumeration crashes! 2016-01-06 17:43:01 -08:00
Ryan Olson 85424fd15e Support viewing AFNetworking request bodies 2016-01-05 15:10:10 -08:00
Ryan Olson cff391f78c Use NSKeyedArchiver for files with .coded extensions 2015-12-18 19:38:32 -05:00
Ryan Olson 3e420bb747 Bump version in .podspec 2015-12-14 08:56:10 -08:00
Ryan Olson 8aece0a266 Update README 2015-12-14 08:31:42 -08:00
Ryan Olson 81b27b6918 Modify approach to toggling views and menu modals.
This approach seems to eliminate some of the status bar issues we were seeing.
2015-12-13 23:21:43 -08:00
Ryan Olson 727943c4b3 Merge pull request #92 from WangHengHeng/master
- (void)toggle***Tool - It is not necessary to 'dismiss' when 'present'
2015-12-13 23:12:41 -08:00
Ryan Olson 9f2c032157 List FLEX.h in the public headers in the podspec 2015-12-13 23:06:40 -08:00
Ryan Olson d6a5b1af8d Bump podspec iOS version to 8.0 2015-12-13 23:03:09 -08:00
Ryan Olson dda9dd5beb Move to UISearchController in FLEXSystemLogTableViewController 2015-12-13 22:55:57 -08:00
Ryan Olson 888887f09a Move to UISearchController in FLEXNetworkHistoryTableViewController 2015-12-13 22:46:46 -08:00
Ryan Olson b70a1a2f48 Move to UISearchController in FLEXFileBrowserTableViewController 2015-12-13 22:15:45 -08:00
Ryan Olson 54730c368c Use FLEX.framework in example project
Bump example project deployment target to iOS 8 so we can link to the dynamic framework.
2015-12-13 21:09:20 -08:00
Ryan Olson 21672e6f8d Merge pull request #93 from tttpeng/master
Add a sqlite database browser
2015-12-12 19:11:12 -08:00
Taavo 4ffc992872 Fix some problems about database browser 2015-12-06 04:32:26 +08:00
tttpeng 8eea2ec652 Remove useless methods, About sqlite browser 2015-12-02 12:00:40 +08:00
王 原闯 3df01ee7bb dismiss previous present viewController before present a new one 2015-12-02 10:53:55 +08:00
Ryan Olson d0ad6e4319 Merge pull request #94 from untouchable741/master
Support searching for view pointer address in FLEXHierarchyTableViewC…
2015-12-01 08:26:43 -08:00
Tai Vuong 37aec6dacc Support searching for view pointer address in FLEXHierarchyTableViewController 2015-12-01 22:32:24 +07:00
王 原闯 cdc5aae4b7 - (void)toggle***Tool - It is not necessary to 'dismiss' when 'present' 2015-11-30 18:01:19 +08:00
tttpeng fd2b89fd24 Add sqlite database browser 2015-11-30 17:36:41 +08:00
Ryan Olson f1683e54c3 Add support for force touch in the simulator 2015-11-16 11:37:57 -08:00
Ryan Olson c66dd2e7d3 Merge pull request #85 from dlo/master
Use Objective-C 2.0 subscripting
2015-11-09 06:29:55 -08:00
Ryan Olson 5a5b921bbf Merge pull request #89 from revolter/patch-1
Update FLEXManager.m
2015-11-09 06:27:26 -08:00
Iulian Onofrei 30cc65bd9d Update FLEXManager.m
Fix help screen typo
2015-11-09 11:18:29 +02:00
Dan Loewenherz 29a45aa02d use Objective-C 2.0 subscripting for dictionaries 2015-10-31 17:37:45 -05:00
Dan Loewenherz 08b25ea8d3 use Objective-C 2.0 subscripting for arrays 2015-10-31 17:35:47 -05:00
Ryan Olson 7ffcb83563 Bump version in FLEX.podspec 2015-10-28 20:05:54 -07:00
688 changed files with 46654 additions and 21889 deletions
+3
View File
@@ -17,3 +17,6 @@ DerivedData
*.ipa
*.xcuserstate
.DS_Store
/Example/Pods
Podfile.lock
IDEWorkspaceChecks.plist
+6 -2
View File
@@ -1,8 +1,12 @@
language: objective-c
xcode_workspace: FLEX.xcworkspace
xcode_sdk: iphonesimulator
before_install:
- gem install xcpretty
matrix:
include:
- xcode_scheme: UICatalog
xcode_sdk: iphonesimulator
- xcode_scheme: FLEX
xcode_sdk: iphonesimulator
script:
- set -o pipefail
- xcodebuild -workspace $TRAVIS_XCODE_WORKSPACE -scheme $TRAVIS_XCODE_SCHEME -sdk $TRAVIS_XCODE_SDK build | xcpretty
+3
View File
@@ -0,0 +1,3 @@
# Contributing to FLEX #
We welcome contributions! Please open a pull request with your changes.
@@ -0,0 +1,89 @@
//
// FLEXFilteringTableViewController.h
// FLEX
//
// Created by Tanner on 3/9/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#pragma mark - FLEXTableViewFiltering
@protocol FLEXTableViewFiltering <FLEXSearchResultsUpdating>
/// An array of visible, "filtered" sections. For example,
/// if you have 3 sections in \c allSections and the user searches
/// for something that matches rows in only one section, then
/// this property would only contain that on matching section.
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
/// An array of all possible sections. Empty sections are to be removed
/// and the resulting array stored in the \c section property. Setting
/// this property should immediately set \c sections to \c nonemptySections
///
/// Do not manually initialize this property, it will be
/// initialized for you using the result of \c makeSections.
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
/// This computed property should filter \c allSections for assignment to \c sections
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
/// This should be able to re-initialize \c allSections
- (NSArray<FLEXTableViewSection *> *)makeSections;
@end
#pragma mark - FLEXFilteringTableViewController
/// A table view which implements \c UITableView* methods using arrays of
/// \c FLEXTableViewSection objects provied by a special delegate.
@interface FLEXFilteringTableViewController : FLEXTableViewController <FLEXTableViewFiltering>
/// Stores the current search query.
@property (nonatomic, copy) NSString *filterText;
/// This property is set to \c self by default.
///
/// This property is used to power almost all of the table view's data source
/// and delegate methods automatically, including row and section filtering
/// when the user searches, 3D Touch context menus, row selection, etc.
///
/// Setting this property will also set \c searchDelegate to that object.
@property (nonatomic, weak) id<FLEXTableViewFiltering> filterDelegate;
/// Defaults to \c NO. If enabled, all filtering will be done by calling
/// \c onBackgroundQueue:thenOnMainQueue: with the UI updated on the main queue.
@property (nonatomic) BOOL filterInBackground;
/// Defaults to \c NO. If enabled, one • will be supplied as an index title for each section.
@property (nonatomic) BOOL wantsSectionIndexTitles;
/// Recalculates the non-empty sections and reloads the table view.
///
/// Subclasses may override to perform additional reloading logic,
/// such as calling \c -reloadSections if needed. Be sure to call
/// \c super after any logic that would affect the appearance of
/// the table view, since the table view is reloaded last.
///
/// Called at the end of this class's implementation of \c updateSearchResults:
- (void)reloadData;
/// Invoke this method to call \c -reloadData on each section
/// in \c self.filterDelegate.allSections.
- (void)reloadSections;
#pragma mark FLEXTableViewFiltering
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
/// Subclasses can override to hide specific sections under certain conditions
/// if using \c self as the \c filterDelegate, as is the default.
///
/// For example, the object explorer hides the description section when searching.
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
/// If using \c self as the \c filterDelegate, as is the default,
/// subclasses should override to provide the sections for the table view.
- (NSArray<FLEXTableViewSection *> *)makeSections;
@end
@@ -0,0 +1,203 @@
//
// FLEXFilteringTableViewController.m
// FLEX
//
// Created by Tanner on 3/9/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
#import "FLEXTableViewSection.h"
#import "NSArray+FLEX.h"
@interface FLEXFilteringTableViewController ()
@end
@implementation FLEXFilteringTableViewController
@synthesize allSections = _allSections;
#pragma mark - View controller lifecycle
- (void)loadView {
[super loadView];
if (!self.filterDelegate) {
self.filterDelegate = self;
} else {
[self _registerCellsForReuse];
}
}
- (void)_registerCellsForReuse {
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
if (section.cellRegistrationMapping) {
[self.tableView registerCells:section.cellRegistrationMapping];
}
}
}
#pragma mark - Public
- (void)setFilterDelegate:(id<FLEXTableViewFiltering>)filterDelegate {
_filterDelegate = filterDelegate;
filterDelegate.allSections = [filterDelegate makeSections];
if (self.isViewLoaded) {
[self _registerCellsForReuse];
}
}
- (void)reloadData {
[self reloadData:self.nonemptySections];
}
- (void)reloadData:(NSArray *)nonemptySections {
// Recalculate displayed sections
self.filterDelegate.sections = nonemptySections;
// Refresh table view
if (self.isViewLoaded) {
[self.tableView reloadData];
}
}
- (void)reloadSections {
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
[section reloadData];
}
}
#pragma mark - Search
- (void)updateSearchResults:(NSString *)newText {
NSArray *(^filter)() = ^NSArray *{
self.filterText = newText;
// Sections will adjust data based on this property
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
section.filterText = newText;
}
return nil;
};
if (self.filterInBackground) {
[self onBackgroundQueue:filter thenOnMainQueue:^(NSArray *unused) {
if ([self.searchText isEqualToString:newText]) {
[self reloadData];
}
}];
} else {
filter();
[self reloadData];
}
}
#pragma mark Filtering
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
return [self.filterDelegate.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
return section.numberOfRows > 0;
}];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
return @[];
}
- (void)setAllSections:(NSArray<FLEXTableViewSection *> *)allSections {
_allSections = allSections.copy;
self.sections = self.nonemptySections;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.filterDelegate.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filterDelegate.sections[section].numberOfRows;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.filterDelegate.sections[section].title;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *reuse = [self.filterDelegate.sections[indexPath.section] reuseIdentifierForRow:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath];
[self.filterDelegate.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (self.wantsSectionIndexTitles) {
return [NSArray flex_forEachUpTo:self.filterDelegate.sections.count map:^id(NSUInteger i) {
return @"";
}];
}
return nil;
}
#pragma mark - UITableViewDelegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.filterDelegate.sections[indexPath.section] canSelectRow:indexPath.row];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
if (action) {
action(self);
[tableView deselectRowAtIndexPath:indexPath animated:YES];
} else if (details) {
[self.navigationController pushViewController:details animated:YES];
} else {
[NSException raise:NSInternalInconsistencyException
format:@"Row is selectable but has no action or view controller"];
}
}
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
[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];
NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self];
if (menuItems.count) {
return [UIContextMenuConfiguration
configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
return [UIMenu menuWithTitle:title children:menuItems];
}
];
}
return nil;
}
#endif
@end
@@ -0,0 +1,19 @@
//
// FLEXNavigationController.h
// FLEX
//
// Created by Tanner on 1/30/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXNavigationController : UINavigationController
+ (instancetype)withRootViewController:(UIViewController *)rootVC;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,178 @@
//
// FLEXNavigationController.m
// FLEX
//
// Created by Tanner on 1/30/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXNavigationController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXTabList.h"
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
@end
@interface UIPanGestureRecognizer (Private)
- (void)_setDelegate:(id)delegate;
@end
@interface FLEXNavigationController ()
@property (nonatomic, readonly) BOOL toolbarWasHidden;
@property (nonatomic) BOOL waitingToAddTab;
@property (nonatomic) BOOL didSetupPendingDismissButtons;
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
@end
@implementation FLEXNavigationController
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
return [[self alloc] initWithRootViewController:rootVC];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.waitingToAddTab = YES;
// Add gesture to reveal toolbar if hidden
self.navigationBar.userInteractionEnabled = YES;
[self.navigationBar addGestureRecognizer:[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleNavigationBarTap:)
]];
// Add gesture to dismiss if not presented with a sheet style
if (@available(iOS 13, *)) {
switch (self.modalPresentationStyle) {
case UIModalPresentationAutomatic:
case UIModalPresentationPageSheet:
case UIModalPresentationFormSheet:
break;
default:
[self addNavigationBarSwipeGesture];
break;
}
} else {
[self addNavigationBarSwipeGesture];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.beingPresented && !self.didSetupPendingDismissButtons) {
for (UIViewController *vc in self.viewControllers) {
[self addNavigationBarItemsToViewController:vc.navigationItem];
}
self.didSetupPendingDismissButtons = YES;
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.waitingToAddTab) {
// Only add new tab if we're presented properly
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
// New navigation controllers always add themselves as new tabs,
// tabs are closed by FLEXExplorerViewController
[FLEXTabList.sharedList addTab:self];
self.waitingToAddTab = NO;
}
}
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
[super pushViewController:viewController animated:animated];
[self addNavigationBarItemsToViewController:viewController.navigationItem];
}
- (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];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
if (!self.presentingViewController) {
return;
}
// Check if a done item already exists
for (UIBarButtonItem *item in navigationItem.rightBarButtonItems) {
if (item.style == UIBarButtonItemStyleDone) {
return;
}
}
// Give root view controllers a Done button if it does not already have one
UIBarButtonItem *done = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(dismissAnimated)
];
// Prepend the button if other buttons exist already
NSArray *existingItems = navigationItem.rightBarButtonItems;
if (existingItems.count) {
navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems];
} else {
navigationItem.rightBarButtonItem = done;
}
// Keeps us from calling this method again on
// the same view controllers in -viewWillAppear:
self.didSetupPendingDismissButtons = YES;
}
- (void)addNavigationBarSwipeGesture {
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(handleNavigationBarSwipe:)
];
swipe.direction = UISwipeGestureRecognizerDirectionDown;
swipe.delegate = self;
self.navigationBarSwipeGesture = swipe;
[self.navigationBar addGestureRecognizer:swipe];
}
- (void)handleNavigationBarSwipe:(UISwipeGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)handleNavigationBarTap:(UIGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
if (self.toolbarHidden) {
[self setToolbarHidden:NO animated:YES];
}
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
if (g1 == self.navigationBarSwipeGesture && g2 == self.barHideOnSwipeGestureRecognizer) {
return YES;
}
return NO;
}
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
BOOL show = self.topViewController.toolbarItems.count;
CGFloat yTranslation = [sender translationInView:self.view].y;
CGFloat yVelocity = [sender velocityInView:self.view].y;
if (yVelocity > 2000) {
[self setToolbarHidden:YES animated:YES];
} else if (show && yTranslation > 20 && yVelocity > 250) {
[self setToolbarHidden:NO animated:YES];
} else if (yTranslation < -20) {
[self setToolbarHidden:YES animated:YES];
}
}
}
@end
@@ -0,0 +1,149 @@
//
// FLEXTableViewController.h
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableView.h"
@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
typedef CGFloat FLEXDebounceInterval;
/// No delay, all events delivered
extern CGFloat const kFLEXDebounceInstant;
/// Small delay which makes UI seem smoother by avoiding rapid events
extern CGFloat const kFLEXDebounceFast;
/// Slower than Fast, faster than ExpensiveIO
extern CGFloat const kFLEXDebounceForAsyncSearch;
/// The least frequent, at just over once per second; for I/O or other expensive operations
extern CGFloat const kFLEXDebounceForExpensiveIO;
@protocol FLEXSearchResultsUpdating <NSObject>
/// A method to handle search query update events.
///
/// \c searchBarDebounceInterval is used to reduce the frequency at which this
/// method is called. This method is also called when the search bar becomes
/// the first responder, and when the selected search bar scope index changes.
- (void)updateSearchResults:(NSString *)newText;
@end
@interface FLEXTableViewController : UITableViewController <
UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate
>
/// A grouped table view. Inset on iOS 13.
///
/// Simply calls into \c initWithStyle:
- (id)init;
/// Subclasses may override to configure the controller before \c viewDidLoad:
- (id)initWithStyle:(UITableViewStyle)style;
@property (nonatomic) FLEXTableView *tableView;
/// If your subclass conforms to \c FLEXSearchResultsUpdating
/// then this property is assigned to \c self automatically.
///
/// Setting \c filterDelegate will also set this property to that object.
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchDelegate;
/// Defaults to NO.
///
/// Setting this to YES will initialize the carousel and the view.
@property (nonatomic) BOOL showsCarousel;
/// A horizontally scrolling list with functionality similar to
/// that of a search bar's scope bar. You'd want to use this when
/// you have potentially more than 4 scope options.
@property (nonatomic) FLEXScopeCarousel *carousel;
/// Defaults to NO.
///
/// Setting this to YES will initialize searchController and the view.
@property (nonatomic) BOOL showsSearchBar;
/// Defaults to NO.
///
/// Setting this to YES will make the search bar appear whenever the view appears.
/// Otherwise, iOS will only show the search bar when you scroll up.
@property (nonatomic) BOOL showSearchBarInitially;
/// nil unless showsSearchBar is set to YES.
///
/// self is used as the default search results updater and delegate.
/// The search bar will not dim the background or hide the navigation bar by default.
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
@property (nonatomic) UISearchController *searchController;
/// Used to initialize the search controller. Defaults to nil.
@property (nonatomic) UIViewController *searchResultsController;
/// Defaults to "Fast"
///
/// Determines how often search bar results will be "debounced."
/// Empty query events are always sent instantly. Query events will
/// be sent when the user has not changed the query for this interval.
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
/// Whether the search bar stays at the top of the view while scrolling.
///
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
/// or it will not be respsected. Use this instead.
/// Defaults to NO.
@property (nonatomic) BOOL pinSearchBar;
/// By default, we will show the search bar's cancel button when
/// search becomes active and hide it when search is dismissed.
///
/// Do not set the showsCancelButton property on the searchController's
/// searchBar manually. Set this property after turning on showsSearchBar.
///
/// Does nothing pre-iOS 13, safe to call on any version.
@property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton;
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
@property (nonatomic) NSInteger selectedScope;
/// self.searchController.searchBar.text
@property (nonatomic, readonly) NSString *searchText;
/// A totally optional delegate to forward search results updater calls to.
/// If a delegate is set, updateSearchResults: is not called on this view controller.
@property (nonatomic, weak ) id<FLEXSearchResultsUpdating> searchResultsUpdater;
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
/// Convenient for doing some async processor-intensive searching
/// in the background before updating the UI back on the main queue.
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
/// Adds up to 3 additional items to the toolbar in right-to-left order.
///
/// That is, the first item in the given array will be the rightmost item behind
/// any existing toolbar items. By default, buttons for bookmarks and tabs are shown.
///
/// If you wish to have more control over how the buttons are arranged or which
/// buttons are displayed, you can access the properties for the pre-existing
/// toolbar items directly and manually set \c self.toolbarItems by overriding
/// the \c setupToolbarItems method below.
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items;
/// Subclasses may override. You should not need to call this method directly.
- (void)setupToolbarItems;
@property (nonatomic, readonly) UIBarButtonItem *shareToolbarItem;
@property (nonatomic, readonly) UIBarButtonItem *bookmarksToolbarItem;
@property (nonatomic, readonly) UIBarButtonItem *openTabsToolbarItem;
/// Whether or not to display the "share" icon in the middle of the toolbar. NO by default.
///
/// Turning this on after you have added custom toolbar items will
/// push off the leftmost toolbar item and shift the others leftward.
@property (nonatomic) BOOL showsShareToolbarItem;
/// Called when the share button is pressed.
/// Default implementation does nothign. Subclasses may override.
- (void)shareButtonPressed:(UIBarButtonItem *)sender;
/// Subclasses may call this to opt-out of all toolbar related behavior.
/// This is necessary if you want to disable the gesture which reveals the toolbar.
- (void)disableToolbar;
@end
@@ -0,0 +1,590 @@
//
// FLEXTableViewController.m
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXBookmarksViewController.h"
#import "FLEXTabsViewController.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXResources.h"
#import "UIBarButtonItem+FLEX.h"
#import <objc/runtime.h>
@interface Block : NSObject
- (void)invoke;
@end
CGFloat const kFLEXDebounceInstant = 0.f;
CGFloat const kFLEXDebounceFast = 0.05;
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
@interface FLEXTableViewController ()
@property (nonatomic) NSTimer *debounceTimer;
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
@property (nonatomic) UITableViewStyle style;
@property (nonatomic) BOOL hasAppeared;
@property (nonatomic, readonly) UIView *tableHeaderViewContainer;
@property (nonatomic, readonly) BOOL manuallyDeactivateSearchOnDisappear;
@property (nonatomic) UIBarButtonItem *middleToolbarItem;
@property (nonatomic) UIBarButtonItem *middleLeftToolbarItem;
@property (nonatomic) UIBarButtonItem *leftmostToolbarItem;
@end
@implementation FLEXTableViewController
@dynamic tableView;
@synthesize showsShareToolbarItem = _showsShareToolbarItem;
@synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
#pragma mark - Initialization
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
_style = style;
_manuallyDeactivateSearchOnDisappear = ({
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
});
// We will be our own search delegate if we implement this method
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
self.searchDelegate = (id)self;
}
}
return self;
}
#pragma mark - Public
- (FLEXWindow *)window {
return (id)self.view.window;
}
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
if (_showsSearchBar == showsSearchBar) return;
_showsSearchBar = showsSearchBar;
if (showsSearchBar) {
UIViewController *results = self.searchResultsController;
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
self.searchController.searchBar.placeholder = @"Filter";
self.searchController.searchResultsUpdater = (id)self;
self.searchController.delegate = (id)self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.hidesNavigationBarDuringPresentation = NO;
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
self.searchController.searchBar.delegate = self;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
[self addSearchController:self.searchController];
} else {
// Search already shown and just set to NO, so remove it
[self removeSearchController:self.searchController];
}
}
- (void)setShowsCarousel:(BOOL)showsCarousel {
if (_showsCarousel == showsCarousel) return;
_showsCarousel = showsCarousel;
if (showsCarousel) {
_carousel = ({
__weak __typeof(self) weakSelf = self;
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
[self.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;
[self layoutTableHeaderIfNeeded];
}];
carousel;
});
[self addCarousel:_carousel];
} else {
// Carousel already shown and just set to NO, so remove it
[self removeCarousel:_carousel];
}
}
- (NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
return self.searchController.searchBar.selectedScopeButtonIndex;
} else if (self.showsCarousel) {
return self.carousel.selectedIndex;
} else {
return 0;
}
}
- (void)setSelectedScope:(NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
self.searchController.searchBar.selectedScopeButtonIndex = selectedScope;
} else if (self.showsCarousel) {
self.carousel.selectedIndex = selectedScope;
}
[self.searchDelegate updateSearchResults:self.searchText];
}
- (NSString *)searchText {
return self.searchController.searchBar.text;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *items = backgroundBlock();
dispatch_async(dispatch_get_main_queue(), ^{
mainBlock(items);
});
});
}
- (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem {
_showsShareToolbarItem = showsShareToolbarItem;
if (self.isViewLoaded) {
[self setupToolbarItems];
}
}
- (void)disableToolbar {
self.navigationController.toolbarHidden = YES;
self.navigationController.hidesBarsOnSwipe = NO;
self.toolbarItems = nil;
}
#pragma mark - View Controller Lifecycle
- (void)loadView {
self.view = [FLEXTableView style:self.style];
self.tableView.dataSource = self;
self.tableView.delegate = self;
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:));
_bookmarksToolbarItem = [UIBarButtonItem
itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
];
_openTabsToolbarItem = [UIBarButtonItem
itemWithImage:FLEXResources.openTabsIcon target:self action:@selector(showTabSwitcher)
];
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
self.middleLeftToolbarItem = UIBarButtonItem.flex_fixedSpace;
self.middleToolbarItem = UIBarButtonItem.flex_fixedSpace;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// Toolbar
self.navigationController.toolbarHidden = NO;
self.navigationController.hidesBarsOnSwipe = YES;
// On iOS 13, the root view controller shows it's search bar no matter what.
// Turning this off avoids some weird flash the navigation bar does when we
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
// will still happen on subsequent view controllers, but we can at least
// avoid it for the root view controller
if (@available(iOS 13, *)) {
if (self.navigationController.viewControllers.firstObject == self) {
_showSearchBarInitially = NO;
}
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
[self setupToolbarItems];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Allow scrolling to collapse the search bar, only if we don't want it pinned
if (@available(iOS 11.0, *)) {
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
// the search bar appear initially results in a bugged search bar that
// becomes transparent and floats over the screen as you scroll
[UIView animateWithDuration:0.2 animations:^{
self.navigationItem.hidesSearchBarWhenScrolling = YES;
[self.navigationController.view setNeedsLayout];
[self.navigationController.view layoutIfNeeded];
}];
}
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.manuallyDeactivateSearchOnDisappear && self.searchController.isActive) {
self.searchController.active = NO;
}
}
- (void)didMoveToParentViewController:(UIViewController *)parent {
[super didMoveToParentViewController:parent];
// Reset this since we are re-appearing under a new
// parent view controller and need to show it again
self.didInitiallyRevealSearchBar = NO;
}
#pragma mark - Toolbar, Public
- (void)setupToolbarItems {
if (!self.isViewLoaded) {
return;
}
self.toolbarItems = @[
self.leftmostToolbarItem,
UIBarButtonItem.flex_flexibleSpace,
self.middleLeftToolbarItem,
UIBarButtonItem.flex_flexibleSpace,
self.middleToolbarItem,
UIBarButtonItem.flex_flexibleSpace,
self.bookmarksToolbarItem,
UIBarButtonItem.flex_flexibleSpace,
self.openTabsToolbarItem,
];
for (UIBarButtonItem *item in self.toolbarItems) {
[item _setWidth:60];
// This does not work for anything but fixed spaces for some reason
// item.width = 60;
}
// Disable tabs entirely when not presented by FLEXExplorerViewController
UIViewController *presenter = self.navigationController.presentingViewController;
if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
self.openTabsToolbarItem.enabled = NO;
}
}
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items {
if (self.showsShareToolbarItem) {
// Share button is in the middle, skip middle button
if (items.count > 0) {
self.middleLeftToolbarItem = items[0];
}
if (items.count > 1) {
self.leftmostToolbarItem = items[1];
}
} else {
// Add buttons right-to-left
if (items.count > 0) {
self.middleToolbarItem = items[0];
}
if (items.count > 1) {
self.middleLeftToolbarItem = items[1];
}
if (items.count > 2) {
self.leftmostToolbarItem = items[2];
}
}
[self setupToolbarItems];
}
- (void)setShowsShareToolbarItem:(BOOL)showShare {
if (_showsShareToolbarItem != showShare) {
_showsShareToolbarItem = showShare;
if (showShare) {
// Push out leftmost item
self.leftmostToolbarItem = self.middleLeftToolbarItem;
self.middleLeftToolbarItem = self.middleToolbarItem;
// Use share for middle
self.middleToolbarItem = self.shareToolbarItem;
} else {
// Remove share, shift custom items rightward
self.middleToolbarItem = self.middleLeftToolbarItem;
self.middleLeftToolbarItem = self.leftmostToolbarItem;
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
}
}
[self setupToolbarItems];
}
- (void)shareButtonPressed:(UIBarButtonItem *)sender {
}
#pragma mark - Private
- (void)debounce:(void(^)(void))block {
[self.debounceTimer invalidate];
self.debounceTimer = [NSTimer
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
target:block
selector:@selector(invoke)
userInfo:nil
repeats:NO
];
}
- (void)layoutTableHeaderIfNeeded {
if (self.showsCarousel) {
self.carousel.frame = FLEXRectSetHeight(
self.carousel.frame, self.carousel.intrinsicContentSize.height
);
}
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}
- (void)addCarousel:(FLEXScopeCarousel *)carousel {
if (@available(iOS 11.0, *)) {
self.tableView.tableHeaderView = carousel;
} else {
carousel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
CGRect frame = self.tableHeaderViewContainer.frame;
CGRect subviewFrame = carousel.frame;
subviewFrame.origin.y = 0;
// Put the carousel below the search bar if it's already there
if (self.showsSearchBar) {
carousel.frame = subviewFrame = FLEXRectSetY(
subviewFrame, self.searchController.searchBar.frame.size.height
);
frame.size.height += carousel.intrinsicContentSize.height;
} else {
frame.size.height = carousel.intrinsicContentSize.height;
}
self.tableHeaderViewContainer.frame = frame;
[self.tableHeaderViewContainer addSubview:carousel];
}
[self layoutTableHeaderIfNeeded];
}
- (void)removeCarousel:(FLEXScopeCarousel *)carousel {
[carousel removeFromSuperview];
if (@available(iOS 11.0, *)) {
self.tableView.tableHeaderView = nil;
} else {
if (self.showsSearchBar) {
[self removeSearchController:self.searchController];
[self addSearchController:self.searchController];
} else {
self.tableView.tableHeaderView = nil;
_tableHeaderViewContainer = nil;
}
}
}
- (void)addSearchController:(UISearchController *)controller {
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = controller;
} else {
controller.searchBar.autoresizingMask |= UIViewAutoresizingFlexibleBottomMargin;
[self.tableHeaderViewContainer addSubview:controller.searchBar];
CGRect subviewFrame = controller.searchBar.frame;
CGRect frame = self.tableHeaderViewContainer.frame;
frame.size.width = MAX(frame.size.width, subviewFrame.size.width);
frame.size.height = subviewFrame.size.height;
// Move the carousel down if it's already there
if (self.showsCarousel) {
self.carousel.frame = FLEXRectSetY(
self.carousel.frame, subviewFrame.size.height
);
frame.size.height += self.carousel.frame.size.height;
}
self.tableHeaderViewContainer.frame = frame;
[self layoutTableHeaderIfNeeded];
}
}
- (void)removeSearchController:(UISearchController *)controller {
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = nil;
} else {
[controller.searchBar removeFromSuperview];
if (self.showsCarousel) {
// self.carousel.frame = FLEXRectRemake(CGPointZero, self.carousel.frame.size);
[self removeCarousel:self.carousel];
[self addCarousel:self.carousel];
} else {
self.tableView.tableHeaderView = nil;
_tableHeaderViewContainer = nil;
}
}
}
- (UIView *)tableHeaderViewContainer {
if (!_tableHeaderViewContainer) {
_tableHeaderViewContainer = [UIView new];
self.tableView.tableHeaderView = self.tableHeaderViewContainer;
}
return _tableHeaderViewContainer;
}
- (void)showBookmarks {
UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:[FLEXBookmarksViewController new]
];
[self presentViewController:nav animated:YES completion:nil];
}
- (void)showTabSwitcher {
UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:[FLEXTabsViewController new]
];
[self presentViewController:nav animated:YES completion:nil];
}
#pragma mark - Search Bar
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[self.debounceTimer invalidate];
NSString *text = searchController.searchBar.text;
void (^updateSearchResults)() = ^{
if (self.searchResultsUpdater) {
[self.searchResultsUpdater updateSearchResults:text];
} else {
[self.searchDelegate updateSearchResults:text];
}
};
// Only debounce if we want to, and if we have a non-empty string
// Empty string events are sent instantly
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
[self debounce:updateSearchResults];
} else {
updateSearchResults();
}
}
#pragma mark UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController *)searchController {
// Manually show cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:YES animated:YES];
}
}
- (void)willDismissSearchController:(UISearchController *)searchController {
// Manually hide cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:NO animated:YES];
}
}
#pragma mark UISearchBarDelegate
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
[self updateSearchResultsForSearchController:self.searchController];
}
#pragma mark Table View
/// Not having a title in the first section looks weird with a rounded-corner table view style
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (@available(iOS 13, *)) {
if (self.style == UITableViewStyleInsetGrouped) {
return @" ";
}
}
return nil; // For plain/gropued style
}
@end
+28
View File
@@ -0,0 +1,28 @@
//
// FLEXSingleRowSection.h
// FLEX
//
// Created by Tanner Bennett on 9/25/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
/// A section providing a specific single row.
///
/// You may optionally provide a view controller to push when the row
/// is selected, or an action to perform when it is selected.
/// Which one is used first is up to the table view data source.
@interface FLEXSingleRowSection : FLEXTableViewSection
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
+ (instancetype)title:(NSString *)sectionTitle
reuse:(NSString *)reuseIdentifier
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
@property (nonatomic) UIViewController *pushOnSelection;
@property (nonatomic) void (^selectionAction)(UIViewController *host);
/// Called to determine whether the single row should display itself or not.
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
@end
+87
View File
@@ -0,0 +1,87 @@
//
// FLEXSingleRowSection.m
// FLEX
//
// Created by Tanner Bennett on 9/25/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXSingleRowSection.h"
#import "FLEXTableView.h"
@interface FLEXSingleRowSection ()
@property (nonatomic, readonly) NSString *reuseIdentifier;
@property (nonatomic, readonly) void (^cellConfiguration)(__kindof UITableViewCell *cell);
@property (nonatomic) NSString *lastTitle;
@property (nonatomic) NSString *lastSubitle;
@end
@implementation FLEXSingleRowSection
#pragma mark - Public
+ (instancetype)title:(NSString *)title
reuse:(NSString *)reuse
cell:(void (^)(__kindof UITableViewCell *))config {
return [[self alloc] initWithTitle:title reuse:reuse cell:config];
}
- (id)initWithTitle:(NSString *)sectionTitle
reuse:(NSString *)reuseIdentifier
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
self = [super init];
if (self) {
_title = sectionTitle;
_reuseIdentifier = reuseIdentifier ?: kFLEXDefaultCell;
_cellConfiguration = cellConfiguration;
}
return self;
}
#pragma mark - Overrides
- (NSInteger)numberOfRows {
if (self.filterMatcher && self.filterText.length) {
return self.filterMatcher(self.filterText) ? 1 : 0;
}
return 1;
}
- (BOOL)canSelectRow:(NSInteger)row {
return self.pushOnSelection || self.selectionAction;
}
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
return self.selectionAction;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return self.pushOnSelection;
}
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
return self.reuseIdentifier;
}
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
cell.textLabel.text = nil;
cell.detailTextLabel.text = nil;
cell.accessoryType = UITableViewCellAccessoryNone;
self.cellConfiguration(cell);
self.lastTitle = cell.textLabel.text;
self.lastSubitle = cell.detailTextLabel.text;
}
- (NSString *)titleForRow:(NSInteger)row {
return self.lastTitle;
}
- (NSString *)subtitleForRow:(NSInteger)row {
return self.lastSubitle;
}
@end
+134
View File
@@ -0,0 +1,134 @@
//
// FLEXTableViewSection.h
// FLEX
//
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
NS_ASSUME_NONNULL_BEGIN
#pragma mark FLEXTableViewSection
/// An abstract base class for table view sections.
///
/// Many properties or methods here return nil or some logical equivalent by default.
/// Even so, most of the methods with defaults are intended to be overriden by subclasses.
/// Some methods are not implemented at all and MUST be implemented by a subclass.
@interface FLEXTableViewSection : NSObject {
@protected
/// Unused by default, use if you want
NSString *_title;
}
#pragma mark - Data
/// A title to be displayed for the custom section.
/// Subclasses may override or use the \c _title ivar.
@property (nonatomic, readonly, nullable) NSString *title;
/// The number of rows in this section. Subclasses must override.
/// This should not change until \c filterText is changed or \c reloadData is called.
@property (nonatomic, readonly) NSInteger numberOfRows;
/// A map of reuse identifiers to \c UITableViewCell (sub)class objects.
/// Subclasses \e may override this as necessary, but are not required to.
/// See \c FLEXTableView.h for more information.
/// @return nil by default.
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping;
/// The section should filter itself based on the contents of this property
/// as it is set. If it is set to nil or an empty string, it should not filter.
/// Subclasses should override or observe this property and react to changes.
///
/// It is common practice to use two arrays for the underlying model:
/// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText:
/// is called, call \c super to store the new value, and re-filter your model accordingly.
@property (nonatomic, nullable) NSString *filterText;
/// Provides an avenue for the section to refresh data or change the number of rows.
///
/// This is called before reloading the table view itself. If your section pulls data
/// from an external data source, this is a good place to refresh that data entirely.
/// If your section does not, then it might be simpler for you to just override
/// \c setFilterText: to call \c super and call \c reloadData.
- (void)reloadData;
#pragma mark - Row Selection
/// Whether the given row should be selectable, such as if tapping the cell
/// should take the user to a new screen or trigger an action.
/// Subclasses \e may override this as necessary, but are not required to.
/// @return \c NO by default
- (BOOL)canSelectRow:(NSInteger)row;
/// An action "future" to be triggered when the row is selected, if the row
/// supports being selected as indicated by \c canSelectRow:. Subclasses
/// must implement this in accordance with how they implement \c canSelectRow:
/// if they do not implement \c viewControllerToPushForRow:
/// @return This returns \c nil if no view controller is provided by
/// \c viewControllerToPushForRow: — otherwise it pushes that view controller
/// onto \c host.navigationController
- (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row;
/// A view controller to display when the row is selected, if the row
/// supports being selected as indicated by \c canSelectRow:. Subclasses
/// must implement this in accordance with how they implement \c canSelectRow:
/// if they do not implement \c didSelectRowAction:
/// @return \c nil by default
- (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row;
/// Called when the accessory view's detail button is pressed.
/// @return \c nil by default.
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
- (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
/// Protected, not intended for public use. \c menuTitleForRow:
/// already includes the value returned from this method.
///
/// By default, this returns \c @"". Subclasses may override to
/// provide a detailed description of the target of the context menu.
- (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
/// The context menu items, if any. Subclasses may override.
/// By default, only inludes items for \c copyMenuItemsForRow:.
- (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
/// Subclasses may override to return a list of copiable items.
///
/// Every two elements in the list compose a key-value pair, where the key
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
/// Provide a reuse identifier for the given row. Subclasses should override.
///
/// Custom reuse identifiers should be specified in \c cellRegistrationMapping.
/// You may return any of the identifiers in \c FLEXTableView.h
/// without including them in the \c cellRegistrationMapping.
/// @return \c kFLEXDefaultCell by default.
- (NSString *)reuseIdentifierForRow:(NSInteger)row;
/// Configure a cell for the given row. Subclasses must override.
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row;
#pragma mark - External Convenience
/// For use by whatever view controller uses your section. Not required.
/// @return An optional title.
- (nullable NSString *)titleForRow:(NSInteger)row;
/// For use by whatever view controller uses your section. Not required.
/// @return An optional subtitle.
- (nullable NSString *)subtitleForRow:(NSInteger)row;
@end
NS_ASSUME_NONNULL_END
+128
View File
@@ -0,0 +1,128 @@
//
// FLEXTableViewSection.m
// FLEX
//
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "UIMenu+FLEX.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation FLEXTableViewSection
- (NSInteger)numberOfRows {
return 0;
}
- (void)reloadData { }
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
return nil;
}
- (BOOL)canSelectRow:(NSInteger)row { return NO; }
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
UIViewController *toPush = [self viewControllerToPushForRow:row];
if (toPush) {
return ^(UIViewController *host) {
[host.navigationController pushViewController:toPush animated:YES];
};
}
return nil;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return nil;
}
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
return nil;
}
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
if (subtitle.length) {
return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle];
}
return title;
}
- (NSString *)menuSubtitleForRow:(NSInteger)row {
return @"";
}
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13)) {
NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row];
NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list");
if (copyItems.count) {
NSInteger numberOfActions = copyItems.count / 2;
BOOL collapseMenu = numberOfActions > 4;
UIImage *copyIcon = [UIImage systemImageNamed:@"doc.on.doc"];
NSMutableArray *actions = [NSMutableArray new];
for (NSInteger i = 0; i < copyItems.count; i += 2) {
NSString *key = copyItems[i], *value = copyItems[i+1];
NSString *title = collapseMenu ? key : [@"Copy " stringByAppendingString:key];
UIAction *copy = [UIAction
actionWithTitle:title
image:copyIcon
identifier:nil
handler:^(__kindof UIAction *action) {
UIPasteboard.generalPasteboard.string = value;
}
];
if (!value.length) {
copy.attributes = UIMenuElementAttributesDisabled;
}
[actions addObject:copy];
}
UIMenu *copyMenu = [UIMenu
inlineMenuWithTitle:@"Copy…"
image:copyIcon
children:actions
];
if (collapseMenu) {
return @[[copyMenu collapsed]];
} else {
return @[copyMenu];
}
}
return @[];
}
#endif
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
return nil;
}
- (NSString *)titleForRow:(NSInteger)row { return nil; }
- (NSString *)subtitleForRow:(NSInteger)row { return nil; }
@end
#pragma clang diagnostic pop
@@ -0,0 +1,15 @@
//
// FLEXCarouselCell.h
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXCarouselCell : UICollectionViewCell
@property (nonatomic, copy) NSString *title;
@end
@@ -0,0 +1,93 @@
//
// FLEXCarouselCell.m
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "UIView+FLEX_Layout.h"
@interface FLEXCarouselCell ()
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic, readonly) UIView *selectionIndicatorStripe;
@property (nonatomic) BOOL constraintsInstalled;
@end
@implementation FLEXCarouselCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_titleLabel = [UILabel new];
_selectionIndicatorStripe = [UIView new];
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
if (@available(iOS 10, *)) {
self.titleLabel.adjustsFontForContentSizeCategory = YES;
}
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.selectionIndicatorStripe];
[self installConstraints];
[self updateAppearance];
}
return self;
}
- (void)updateAppearance {
self.selectionIndicatorStripe.hidden = !self.selected;
if (self.selected) {
self.titleLabel.textColor = self.tintColor;
} else {
self.titleLabel.textColor = FLEXColor.deemphasizedTextColor;
}
}
#pragma mark Public
- (NSString *)title {
return self.titleLabel.text;
}
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
[self.titleLabel sizeToFit];
[self setNeedsLayout];
}
#pragma mark Overrides
- (void)prepareForReuse {
[super prepareForReuse];
[self updateAppearance];
}
- (void)installConstraints {
CGFloat stripeHeight = 2;
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
UIView *superview = self.contentView;
[self.titleLabel pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
[self.selectionIndicatorStripe.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = YES;
[self.selectionIndicatorStripe.heightAnchor constraintEqualToConstant:stripeHeight].active = YES;
}
- (void)setSelected:(BOOL)selected {
super.selected = selected;
[self updateAppearance];
}
@end
@@ -0,0 +1,20 @@
//
// FLEXScopeCarousel.h
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
/// Only use on iOS 10 and up. Requires iOS 10 APIs for calculating row sizes.
@interface FLEXScopeCarousel : UIControl
@property (nonatomic, copy) NSArray<NSString *> *items;
@property (nonatomic) NSInteger selectedIndex;
@property (nonatomic) void(^selectedIndexChangedAction)(NSInteger idx);
- (void)registerBlockForDynamicTypeChanges:(void(^)(FLEXScopeCarousel *))handler;
@end
@@ -0,0 +1,204 @@
//
// FLEXScopeCarousel.m
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXScopeCarousel.h"
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "UIView+FLEX_Layout.h"
const CGFloat kCarouselItemSpacing = 0;
NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, readonly) UICollectionView *collectionView;
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
@property (nonatomic, readonly) id dynamicTypeObserver;
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
@property (nonatomic) BOOL constraintsInstalled;
@end
@implementation FLEXScopeCarousel
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = FLEXColor.primaryBackgroundColor;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.translatesAutoresizingMaskIntoConstraints = YES;
_dynamicTypeHandlers = [NSMutableArray new];
CGSize itemSize = CGSizeZero;
if (@available(iOS 10.0, *)) {
itemSize = UICollectionViewFlowLayoutAutomaticSize;
}
// Collection view layout
UICollectionViewFlowLayout *layout = ({
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.sectionInset = UIEdgeInsetsZero;
layout.minimumLineSpacing = kCarouselItemSpacing;
layout.itemSize = itemSize;
layout.estimatedItemSize = itemSize;
layout;
});
// Collection view
_collectionView = ({
UICollectionView *cv = [[UICollectionView alloc]
initWithFrame:CGRectZero
collectionViewLayout:layout
];
cv.showsHorizontalScrollIndicator = NO;
cv.backgroundColor = UIColor.clearColor;
cv.delegate = self;
cv.dataSource = self;
[cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
[self addSubview:cv];
cv;
});
// Sizing cell
_sizingCell = [FLEXCarouselCell new];
self.sizingCell.title = @"NSObject";
// Dynamic type
__weak __typeof(self) weakSelf = self;
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
addObserverForName:UIContentSizeCategoryDidChangeNotification
object:nil queue:nil usingBlock:^(NSNotification *note) {
[self.collectionView setNeedsLayout];
[self setNeedsUpdateConstraints];
// Notify observers
__typeof(self) self = weakSelf;
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
block(self);
}
}
];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver];
}
#pragma mark - Overrides
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGFloat width = 1.f / UIScreen.mainScreen.scale;
// Draw hairline
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, FLEXColor.hairlineColor.CGColor);
CGContextSetLineWidth(context, width);
CGContextMoveToPoint(context, 0, rect.size.height - width);
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
CGContextStrokePath(context);
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.collectionView pinEdgesToSuperview];
self.constraintsInstalled = YES;
}
[super updateConstraints];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(
UIViewNoIntrinsicMetric,
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
);
}
#pragma mark - Public
- (void)setItems:(NSArray<NSString *> *)items {
NSParameterAssert(items.count);
_items = items.copy;
// Refresh list, select first item initially
[self.collectionView reloadData];
self.selectedIndex = 0;
}
- (void)setSelectedIndex:(NSInteger)idx {
NSParameterAssert(idx < self.items.count);
_selectedIndex = idx;
NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
[self.collectionView selectItemAtIndexPath:path
animated:YES
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
}
- (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
[self.dynamicTypeHandlers addObject:handler];
}
#pragma mark - UICollectionView
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
// if (@available(iOS 10.0, *)) {
// return UICollectionViewFlowLayoutAutomaticSize;
// }
self.sizingCell.title = self.items[indexPath.item];
return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.items.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
forIndexPath:indexPath];
cell.title = self.items[indexPath.row];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
_selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
if (self.selectedIndexChangedAction) {
self.selectedIndexChangedAction(indexPath.row);
}
// TODO: dynamically choose a scroll position. Very wide items should
// get "Left" while smaller items should not scroll at all, unless
// they are only partially on the screen, in which case they
// should get "HorizontallyCentered" to bring them onto the screen.
// For now, everything goes to the left, as this has a similar effect.
[collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionLeft
animated:YES];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
@end
@@ -0,0 +1,17 @@
//
// FLEXCodeFontCell.h
// FLEX
//
// Created by Tanner on 12/27/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXMultilineTableViewCell.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
//
// FLEXCodeFontCell.m
// FLEX
//
// Created by Tanner on 12/27/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXCodeFontCell.h"
#import "UIFont+FLEX.h"
@implementation FLEXCodeFontCell
- (void)postInit {
[super postInit];
self.titleLabel.font = UIFont.flex_codeFont;
self.subtitleLabel.font = UIFont.flex_codeFont;
self.titleLabel.adjustsFontSizeToFitWidth = YES;
self.titleLabel.minimumScaleFactor = 0.9;
self.subtitleLabel.adjustsFontSizeToFitWidth = YES;
self.subtitleLabel.minimumScaleFactor = 0.75;
// Disable mutli-line pre iOS 11
if (@available(iOS 11, *)) {
self.subtitleLabel.numberOfLines = 5;
} else {
self.titleLabel.numberOfLines = 1;
self.subtitleLabel.numberOfLines = 1;
}
}
@end
@@ -0,0 +1,13 @@
//
// FLEXKeyValueTableViewCell.h
// FLEX
//
// Created by Tanner Bennett on 1/23/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
@interface FLEXKeyValueTableViewCell : FLEXTableViewCell
@end
@@ -0,0 +1,17 @@
//
// FLEXKeyValueTableViewCell.m
// FLEX
//
// Created by Tanner Bennett on 1/23/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXKeyValueTableViewCell.h"
@implementation FLEXKeyValueTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
}
@end
@@ -0,0 +1,24 @@
//
// FLEXMultilineTableViewCell.h
// FLEX
//
// Created by Ryan Olson on 2/13/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXTableViewCell.h"
/// A cell with both labels set to be multi-line capable.
@interface FLEXMultilineTableViewCell : FLEXTableViewCell
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
maxWidth:(CGFloat)contentViewWidth
style:(UITableViewStyle)style
showsAccessory:(BOOL)showsAccessory;
@end
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
@interface FLEXMultilineDetailTableViewCell : FLEXMultilineTableViewCell
@end
@@ -0,0 +1,67 @@
//
// FLEXMultilineTableViewCell.m
// FLEX
//
// Created by Ryan Olson on 2/13/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXMultilineTableViewCell.h"
#import "UIView+FLEX_Layout.h"
#import "FLEXUtility.h"
@interface FLEXMultilineTableViewCell ()
@property (nonatomic, readonly) UILabel *_titleLabel;
@property (nonatomic, readonly) UILabel *_subtitleLabel;
@property (nonatomic) BOOL constraintsUpdated;
@end
@implementation FLEXMultilineTableViewCell
- (void)postInit {
[super postInit];
self.titleLabel.numberOfLines = 0;
self.subtitleLabel.numberOfLines = 0;
}
+ (UIEdgeInsets)labelInsets {
return UIEdgeInsetsMake(10.0, 16.0, 10.0, 8.0);
}
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
maxWidth:(CGFloat)contentViewWidth
style:(UITableViewStyle)style
showsAccessory:(BOOL)showsAccessory {
CGFloat labelWidth = contentViewWidth;
// Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
if (showsAccessory) {
labelWidth -= 34.0;
}
UIEdgeInsets labelInsets = [self labelInsets];
labelWidth -= (labelInsets.left + labelInsets.right);
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
CGRect boundingBox = [attributedText
boundingRectWithSize:constrainSize
options:NSStringDrawingUsesLineFragmentOrigin
context:nil
];
CGFloat preferredLabelHeight = FLEXFloor(boundingBox.size.height);
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
return preferredCellHeight;
}
@end
@implementation FLEXMultilineDetailTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@end
@@ -0,0 +1,14 @@
//
// FLEXSubtitleTableViewCell.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
/// A cell initialized with \c UITableViewCellStyleSubtitle
@interface FLEXSubtitleTableViewCell : FLEXTableViewCell
@end
@@ -0,0 +1,17 @@
//
// FLEXSubtitleTableViewCell.m
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXSubtitleTableViewCell.h"
@implementation FLEXSubtitleTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@end
@@ -0,0 +1,23 @@
//
// FLEXTableViewCell.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableViewCell : UITableViewCell
/// Use this instead of .textLabel
@property (nonatomic, readonly) UILabel *titleLabel;
/// Use this instead of .detailTextLabel
@property (nonatomic, readonly) UILabel *subtitleLabel;
/// Subclasses can override this instead of initializers to
/// perform additional initialization without lots of boilerplate.
/// Remember to call super!
- (void)postInit;
@end
@@ -0,0 +1,57 @@
//
// FLEXTableViewCell.m
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "FLEXTableView.h"
@interface UITableView (Internal)
// Exists at least since iOS 5
- (BOOL)_canPerformAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
- (void)_performAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
@end
@interface UITableViewCell (Internal)
// Exists at least since iOS 5
@property (nonatomic, readonly) FLEXTableView *_tableView;
@end
@implementation FLEXTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self postInit];
}
return self;
}
- (void)postInit {
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
self.titleLabel.font = cellFont;
self.subtitleLabel.font = cellFont;
self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor;
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
self.titleLabel.numberOfLines = 1;
self.subtitleLabel.numberOfLines = 1;
}
- (UILabel *)titleLabel {
return self.textLabel;
}
- (UILabel *)subtitleLabel {
return self.detailTextLabel;
}
@end
+48
View File
@@ -0,0 +1,48 @@
//
// FLEXTableView.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark Reuse identifiers
typedef NSString * FLEXTableViewCellReuseIdentifier;
/// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault
extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell;
/// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle
extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleDefault
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell;
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell;
/// A \c FLEXTableViewCell initialized with \c UITableViewCellStyleValue1
extern FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell;
/// A \c FLEXSubtitleTableViewCell which uses monospaced fonts for both labels
extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
#pragma mark - FLEXTableView
@interface FLEXTableView : UITableView
+ (instancetype)flexDefaultTableView;
+ (instancetype)groupedTableView;
+ (instancetype)plainTableView;
+ (instancetype)style:(UITableViewStyle)style;
/// You do not need to register classes for any of the default reuse identifiers above
/// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide
/// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell,
/// \c FLEXSubtitleTableViewCell, and \c FLEXMultilineTableViewCell are used, respectively.
///
/// @param registrationMapping A map of reuse identifiers to \c UITableViewCell (sub)class objects.
- (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping;
@end
NS_ASSUME_NONNULL_END
+91
View File
@@ -0,0 +1,91 @@
//
// FLEXTableView.m
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXSubtitleTableViewCell.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXKeyValueTableViewCell.h"
#import "FLEXCodeFontCell.h"
FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell = @"kFLEXDefaultCell";
FLEXTableViewCellReuseIdentifier const kFLEXDetailCell = @"kFLEXDetailCell";
FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell = @"kFLEXMultilineCell";
FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultilineDetailCell";
FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell = @"kFLEXKeyValueCell";
FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
#pragma mark Private
@interface UITableView (Private)
- (CGFloat)_heightForHeaderInSection:(NSInteger)section;
- (NSString *)_titleForHeaderInSection:(NSInteger)section;
@end
@implementation FLEXTableView
+ (instancetype)flexDefaultTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
#pragma mark - Initialization
+ (id)groupedTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
+ (id)plainTableView {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
}
+ (id)style:(UITableViewStyle)style {
return [[self alloc] initWithFrame:CGRectZero style:style];
}
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
self = [super initWithFrame:frame style:style];
if (self) {
[self registerCells:@{
kFLEXDefaultCell : [FLEXTableViewCell class],
kFLEXDetailCell : [FLEXSubtitleTableViewCell class],
kFLEXMultilineCell : [FLEXMultilineTableViewCell class],
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class],
kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class],
kFLEXCodeFontCell : [FLEXCodeFontCell class],
}];
}
return self;
}
#pragma mark - Public
- (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping {
[registrationMapping enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, Class cellClass, BOOL *stop) {
[self registerClass:cellClass forCellReuseIdentifier:identifier];
}];
}
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/30/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/30/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputColorView.h"
@@ -14,8 +14,8 @@
@interface FLEXColorComponentInputView : UIView
@property (nonatomic, strong) UISlider *slider;
@property (nonatomic, strong) UILabel *valueLabel;
@property (nonatomic) UISlider *slider;
@property (nonatomic) UILabel *valueLabel;
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
@@ -30,18 +30,16 @@
@implementation FLEXColorComponentInputView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.slider = [[UISlider alloc] init];
self.slider.backgroundColor = self.backgroundColor;
self.slider = [UISlider new];
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.slider];
self.valueLabel = [[UILabel alloc] init];
self.valueLabel = [UILabel new];
self.valueLabel.backgroundColor = self.backgroundColor;
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
self.valueLabel.font = [UIFont systemFontOfSize:14.0];
self.valueLabel.textAlignment = NSTextAlignmentRight;
[self addSubview:self.valueLabel];
@@ -50,15 +48,13 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.slider.backgroundColor = backgroundColor;
self.valueLabel.backgroundColor = backgroundColor;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
const CGFloat kValueLabelWidth = 50.0;
@@ -73,19 +69,16 @@
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
}
- (void)sliderChanged:(id)sender
{
- (void)sliderChanged:(id)sender {
[self.delegate colorComponentInputViewValueDidChange:self];
[self updateValueLabel];
}
- (void)updateValueLabel
{
- (void)updateValueLabel {
self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = [self.slider sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
@@ -94,52 +87,48 @@
@interface FLEXColorPreviewBox : UIView
@property (nonatomic, strong) UIColor *color;
@property (nonatomic) UIColor *color;
@property (nonatomic, strong) UIView *colorOverlayView;
@property (nonatomic) UIView *colorOverlayView;
@end
@implementation FLEXColorPreviewBox
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1.0;
self.layer.borderColor = [[UIColor blackColor] CGColor];
self.layer.borderColor = UIColor.blackColor.CGColor;
self.backgroundColor = [UIColor colorWithPatternImage:[[self class] backgroundPatternImage]];
self.colorOverlayView = [[UIView alloc] initWithFrame:self.bounds];
self.colorOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.colorOverlayView.backgroundColor = [UIColor clearColor];
self.colorOverlayView.backgroundColor = UIColor.clearColor;
[self addSubview:self.colorOverlayView];
}
return self;
}
- (void)setColor:(UIColor *)color
{
- (void)setColor:(UIColor *)color {
self.colorOverlayView.backgroundColor = color;
}
- (UIColor *)color
{
- (UIColor *)color {
return self.colorOverlayView.backgroundColor;
}
+ (UIImage *)backgroundPatternImage
{
+ (UIImage *)backgroundPatternImage {
const CGFloat kSquareDimension = 5.0;
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [[UIScreen mainScreen] scale]);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale);
[[UIColor whiteColor] setFill];
[UIColor.whiteColor setFill];
UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));
[[UIColor grayColor] setFill];
[UIColor.grayColor setFill];
UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height));
UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height));
@@ -153,55 +142,53 @@
@interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate>
@property (nonatomic, strong) FLEXColorPreviewBox *colorPreviewBox;
@property (nonatomic, strong) UILabel *hexLabel;
@property (nonatomic, strong) FLEXColorComponentInputView *alphaInput;
@property (nonatomic, strong) FLEXColorComponentInputView *redInput;
@property (nonatomic, strong) FLEXColorComponentInputView *greenInput;
@property (nonatomic, strong) FLEXColorComponentInputView *blueInput;
@property (nonatomic) FLEXColorPreviewBox *colorPreviewBox;
@property (nonatomic) UILabel *hexLabel;
@property (nonatomic) FLEXColorComponentInputView *alphaInput;
@property (nonatomic) FLEXColorComponentInputView *redInput;
@property (nonatomic) FLEXColorComponentInputView *greenInput;
@property (nonatomic) FLEXColorComponentInputView *blueInput;
@end
@implementation FLEXArgumentInputColorView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.colorPreviewBox = [[FLEXColorPreviewBox alloc] init];
self.colorPreviewBox = [FLEXColorPreviewBox new];
[self addSubview:self.colorPreviewBox];
self.hexLabel = [[UILabel alloc] init];
self.hexLabel = [UILabel new];
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
self.hexLabel.textAlignment = NSTextAlignmentCenter;
self.hexLabel.font = [FLEXUtility defaultFontOfSize:12.0];
self.hexLabel.font = [UIFont systemFontOfSize:12.0];
[self addSubview:self.hexLabel];
self.alphaInput = [[FLEXColorComponentInputView alloc] init];
self.alphaInput.slider.minimumTrackTintColor = [UIColor blackColor];
self.alphaInput = [FLEXColorComponentInputView new];
self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor;
self.alphaInput.delegate = self;
[self addSubview:self.alphaInput];
self.redInput = [[FLEXColorComponentInputView alloc] init];
self.redInput.slider.minimumTrackTintColor = [UIColor redColor];
self.redInput = [FLEXColorComponentInputView new];
self.redInput.slider.minimumTrackTintColor = UIColor.redColor;
self.redInput.delegate = self;
[self addSubview:self.redInput];
self.greenInput = [[FLEXColorComponentInputView alloc] init];
self.greenInput.slider.minimumTrackTintColor = [UIColor greenColor];
self.greenInput = [FLEXColorComponentInputView new];
self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor;
self.greenInput.delegate = self;
[self addSubview:self.greenInput];
self.blueInput = [[FLEXColorComponentInputView alloc] init];
self.blueInput.slider.minimumTrackTintColor = [UIColor blueColor];
self.blueInput = [FLEXColorComponentInputView new];
self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor;
self.blueInput.delegate = self;
[self addSubview:self.blueInput];
}
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.alphaInput.backgroundColor = backgroundColor;
self.redInput.backgroundColor = backgroundColor;
@@ -209,8 +196,7 @@
self.blueInput.backgroundColor = backgroundColor;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = 0;
@@ -221,14 +207,14 @@
[self.hexLabel sizeToFit];
const CGFloat kLabelVerticalOutsetAmount = 0.0;
const CGFloat kLabelHorizonalOutsetAmount = 2.0;
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount);
const CGFloat kLabelHorizontalOutsetAmount = 2.0;
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount);
self.hexLabel.frame = UIEdgeInsetsInsetRect(self.hexLabel.frame, labelOutset);
CGFloat hexLabelOriginX = self.colorPreviewBox.layer.borderWidth;
CGFloat hexLabelOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) - self.colorPreviewBox.layer.borderWidth - self.hexLabel.frame.size.height;
self.hexLabel.frame = CGRectMake(hexLabelOriginX, hexLabelOriginY, self.hexLabel.frame.size.width, self.hexLabel.frame.size.height);
NSArray *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
NSArray<FLEXColorComponentInputView *> *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
for (FLEXColorComponentInputView *inputView in colorComponentInputViews) {
CGSize fitSize = [inputView sizeThatFits:constrainSize];
inputView.frame = CGRectMake(0, runningOriginY, fitSize.width, fitSize.height);
@@ -236,8 +222,7 @@
}
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[UIColor class]]) {
[self updateWithColor:inputValue];
} else if ([inputValue isKindOfClass:[NSValue class]]) {
@@ -248,21 +233,20 @@
UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
[self updateWithColor:color];
}
} else {
[self updateWithColor:UIColor.clearColor];
}
}
- (id)inputValue
{
- (id)inputValue {
return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value];
}
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView
{
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView {
[self updateColorPreview];
}
- (void)updateWithColor:(UIColor *)color
{
- (void)updateWithColor:(UIColor *)color {
CGFloat red, green, blue, white, alpha;
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
self.alphaInput.slider.value = alpha;
@@ -286,8 +270,7 @@
[self updateColorPreview];
}
- (void)updateColorPreview
{
- (void)updateColorPreview {
self.colorPreviewBox.color = self.inputValue;
unsigned char redByte = self.redInput.slider.value * 255;
unsigned char greenByte = self.greenInput.slider.value * 255;
@@ -296,8 +279,7 @@
[self setNeedsLayout];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = 0;
height += [[self class] colorPreviewBoxHeight];
height += [[self class] inputViewVerticalPadding];
@@ -311,19 +293,19 @@
return CGSizeMake(size.width, height);
}
+ (CGFloat)inputViewVerticalPadding
{
+ (CGFloat)inputViewVerticalPadding {
return 10.0;
}
+ (CGFloat)colorPreviewBoxHeight
{
+ (CGFloat)colorPreviewBoxHeight {
return 40.0;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, @encode(CGColorRef)) == 0 || strcmp(type, FLEXEncodeClass(UIColor)) == 0)) || [value isKindOfClass:[UIColor class]];
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// We don't care if currentValue is a color or not; we will default to +clearColor
return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0);
}
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/14/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/14/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputDateView.h"
@@ -11,17 +11,16 @@
@interface FLEXArgumentInputDateView ()
@property (nonatomic, strong) UIDatePicker *datePicker;
@property (nonatomic) UIDatePicker *datePicker;
@end
@implementation FLEXArgumentInputDateView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.datePicker = [[UIDatePicker alloc] init];
self.datePicker = [UIDatePicker new];
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
// Using UTC, because that's what the NSDate description prints
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
@@ -31,33 +30,29 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSDate class]]) {
self.datePicker.date = inputValue;
}
}
- (id)inputValue
{
- (id)inputValue {
return self.datePicker.date;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.datePicker.frame = self.bounds;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = [self.datePicker sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, FLEXEncodeClass(NSDate)) == 0)) || [value isKindOfClass:[NSDate class]];
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
return strcmp(type, FLEXEncodeClass(NSDate)) == 0;
}
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/28/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/28/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputFontView.h"
@@ -13,25 +13,22 @@
@interface FLEXArgumentInputFontView ()
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
@end
@implementation FLEXArgumentInputFontView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
self.fontNameInput.backgroundColor = self.backgroundColor;
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.fontNameInput.title = @"Font Name:";
[self addSubview:self.fontNameInput];
self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)];
self.pointSizeInput.backgroundColor = self.backgroundColor;
self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.pointSizeInput.title = @"Point Size:";
[self addSubview:self.pointSizeInput];
@@ -39,15 +36,13 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.fontNameInput.backgroundColor = backgroundColor;
self.pointSizeInput.backgroundColor = backgroundColor;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[UIFont class]]) {
UIFont *font = (UIFont *)inputValue;
self.fontNameInput.inputValue = font.fontName;
@@ -55,8 +50,7 @@
}
}
- (id)inputValue
{
- (id)inputValue {
CGFloat pointSize = 0;
if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) {
NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue;
@@ -67,16 +61,14 @@
return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize];
}
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder];
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
@@ -89,13 +81,11 @@
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
}
+ (CGFloat)verticalPaddingBetweenFields
{
+ (CGFloat)verticalPaddingBetweenFields {
return 10.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
@@ -111,11 +101,9 @@
#pragma mark -
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
BOOL supported = type && strcmp(type, FLEXEncodeClass(UIFont)) == 0;
supported = supported || (value && [value isKindOfClass:[UIFont class]]);
return supported;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
return strcmp(type, FLEXEncodeClass(UIFont)) == 0;
}
@end
@@ -1,6 +1,6 @@
//
// FLEXArgumentInputFontsPickerView.h
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
@@ -1,6 +1,6 @@
//
// FLEXArgumentInputFontsPickerView.m
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
@@ -11,15 +11,14 @@
@interface FLEXArgumentInputFontsPickerView ()
@property (nonatomic, strong) NSMutableArray *availableFonts;
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
@end
@implementation FLEXArgumentInputFontsPickerView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
@@ -29,24 +28,21 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
self.inputTextView.text = inputValue;
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
[self.availableFonts insertObject:inputValue atIndex:0];
}
[(UIPickerView*)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
}
- (id)inputValue
{
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
- (id)inputValue {
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
#pragma mark - private
- (UIPickerView*)createFontsPicker
{
- (UIPickerView*)createFontsPicker {
UIPickerView *fontsPicker = [UIPickerView new];
fontsPicker.dataSource = self;
fontsPicker.delegate = self;
@@ -54,10 +50,9 @@
return fontsPicker;
}
- (void)createAvailableFonts
{
NSMutableArray *unsortedFontsArray = [NSMutableArray array];
for (NSString *eachFontFamily in [UIFont familyNames]) {
- (void)createAvailableFonts {
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new];
for (NSString *eachFontFamily in UIFont.familyNames) {
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
[unsortedFontsArray addObject:eachFontName];
}
@@ -67,38 +62,34 @@
#pragma mark - UIPickerViewDataSource
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [self.availableFonts count];
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.availableFonts.count;
}
#pragma mark - UIPickerViewDelegate
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UILabel *fontLabel;
if (!view) {
fontLabel = [UILabel new];
fontLabel.backgroundColor = [UIColor clearColor];
fontLabel.backgroundColor = UIColor.clearColor;
fontLabel.textAlignment = NSTextAlignmentCenter;
} else {
fontLabel = (UILabel*)view;
}
UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0];
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSDictionary<NSString *, id> *attributesDictionary = [NSDictionary<NSString *, id> dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary];
fontLabel.attributedText = attributesString;
[fontLabel sizeToFit];
return fontLabel;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
self.inputTextView.text = self.availableFonts[row];
}
@@ -1,13 +0,0 @@
//
// FLEXArgumentInputJSONObjectView.h
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputTextView.h"
@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
@end
@@ -1,65 +0,0 @@
//
// FLEXArgumentInputJSONObjectView.m
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputJSONObjectView.h"
#import "FLEXRuntimeUtility.h"
@implementation FLEXArgumentInputJSONObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
// square brackets are likely to be the first characters type for the JSON.
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeLarge;
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:inputValue];
}
- (id)inputValue
{
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
// Must be object type.
BOOL supported = type && type[0] == '@';
if (supported) {
if (value) {
// If there's a current value, it must be serializable to JSON
supported = [FLEXRuntimeUtility editableJSONStringForObject:value] != nil;
} else {
// Otherwise, see if we have more type information than just 'id'.
// If we do, make sure the encoding is something serializable to JSON.
// Properties and ivars keep more detailed type encoding information than method arguments.
if (strcmp(type, @encode(id)) != 0) {
BOOL isJSONSerializableType = NO;
// Note: we can't use @encode(NSString) here because that drops the string information and just goes to @encode(id).
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSString)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSNumber)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSArray)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSDictionary)) == 0;
supported = isJSONSerializableType;
}
}
}
return supported;
}
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/18/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputTextView.h"
@@ -3,20 +3,20 @@
// Flipboard
//
// Created by Ryan Olson on 6/18/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputNotSupportedView.h"
#import "FLEXColor.h"
@implementation FLEXArgumentInputNotSupportedView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView.userInteractionEnabled = NO;
self.inputTextView.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1.0];
self.inputTextView.text = @"nil";
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColorWithAlpha:0.5];
self.inputPlaceholderText = @"nil (type not supported)";
self.targetSize = FLEXArgumentInputViewSizeSmall;
}
return self;
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputTextView.h"
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputNumberView.h"
@@ -11,47 +11,52 @@
@implementation FLEXArgumentInputNumberView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeSmall;
}
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue respondsToSelector:@selector(stringValue)]) {
self.inputTextView.text = [inputValue stringValue];
}
}
- (id)inputValue
{
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
- (id)inputValue {
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
static NSArray *primitiveTypes = nil;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
static NSArray<NSString *> *supportedTypes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
primitiveTypes = @[@(@encode(char)),
@(@encode(int)),
@(@encode(short)),
@(@encode(long)),
@(@encode(long long)),
@(@encode(unsigned char)),
@(@encode(unsigned int)),
@(@encode(unsigned short)),
@(@encode(unsigned long)),
@(@encode(unsigned long long)),
@(@encode(float)),
@(@encode(double))];
supportedTypes = @[
@FLEXEncodeClass(NSNumber),
@FLEXEncodeClass(NSDecimalNumber),
@(@encode(char)),
@(@encode(int)),
@(@encode(short)),
@(@encode(long)),
@(@encode(long long)),
@(@encode(unsigned char)),
@(@encode(unsigned int)),
@(@encode(unsigned short)),
@(@encode(unsigned long)),
@(@encode(unsigned long long)),
@(@encode(float)),
@(@encode(double)),
@(@encode(long double))
];
});
return type && [primitiveTypes containsObject:@(type)];
return type && [supportedTypes containsObject:@(type)];
}
@end
@@ -0,0 +1,13 @@
//
// FLEXArgumentInputObjectView.h
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputTextView.h"
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
@end
@@ -0,0 +1,232 @@
//
// FLEXArgumentInputJSONObjectView.m
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputObjectView.h"
#import "FLEXRuntimeUtility.h"
static const CGFloat kSegmentInputMargin = 10;
typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
FLEXArgInputObjectTypeJSON,
FLEXArgInputObjectTypeAddress
};
@interface FLEXArgumentInputObjectView ()
@property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
@property (nonatomic) FLEXArgInputObjectType inputType;
@end
@implementation FLEXArgumentInputObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
// square brackets are likely to be the first characters type for the JSON.
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeLarge;
self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
[self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
self.objectTypeSegmentControl.selectedSegmentIndex = 0;
[self addSubview:self.objectTypeSegmentControl];
self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
}
return self;
}
- (void)didChangeType {
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
if (super.inputValue) {
// Trigger an update to the text field to show
// the address of the stored object we were given,
// or to show a JSON representation of the object
[self populateTextAreaFromValue:super.inputValue];
} else {
// Clear the text field
[self populateTextAreaFromValue:nil];
}
}
- (void)setInputType:(FLEXArgInputObjectType)inputType {
if (_inputType == inputType) return;
_inputType = inputType;
// Resize input view
switch (inputType) {
case FLEXArgInputObjectTypeJSON:
self.targetSize = FLEXArgumentInputViewSizeLarge;
break;
case FLEXArgInputObjectTypeAddress:
self.targetSize = FLEXArgumentInputViewSizeSmall;
break;
}
// Change placeholder
switch (inputType) {
case FLEXArgInputObjectTypeJSON:
self.inputPlaceholderText =
@"You can put any valid JSON here, such as a string, number, array, or dictionary:"
"\n\"This is a string\""
"\n1234"
"\n{ \"name\": \"Bob\", \"age\": 47 }"
"\n["
"\n 1, 2, 3"
"\n]";
break;
case FLEXArgInputObjectTypeAddress:
self.inputPlaceholderText = @"0x0000deadb33f";
break;
}
[self setNeedsLayout];
[self.superview setNeedsLayout];
}
- (void)setInputValue:(id)inputValue {
super.inputValue = inputValue;
[self populateTextAreaFromValue:inputValue];
}
- (id)inputValue {
switch (self.inputType) {
case FLEXArgInputObjectTypeJSON:
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
case FLEXArgInputObjectTypeAddress: {
NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
unsigned long long objectPointerValue;
if ([scanner scanHexLongLong:&objectPointerValue]) {
return (__bridge id)(void *)objectPointerValue;
}
return nil;
}
}
}
- (void)populateTextAreaFromValue:(id)value {
if (!value) {
self.inputTextView.text = nil;
} else {
if (self.inputType == FLEXArgInputObjectTypeJSON) {
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
} else if (self.inputType == FLEXArgInputObjectTypeAddress) {
self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
}
}
// Delegate methods are not called for programmatic changes
[self textViewDidChange:self.inputTextView];
}
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
return fitSize;
}
- (void)layoutSubviews {
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
self.objectTypeSegmentControl.frame = CGRectMake(
0.0,
// Our segmented control is taking the position
// of the text view, as far as super is concerned,
// and we override this property to be different
super.topInputFieldVerticalLayoutGuide,
self.frame.size.width,
segmentHeight
);
[super layoutSubviews];
}
- (CGFloat)topInputFieldVerticalLayoutGuide {
// Our text view is offset from the segmented control
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// Must be object type
return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
}
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
if (value) {
// If there's a current value, it must be serializable to JSON
// to display the JSON editor. Otherwise display the address field.
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
return FLEXArgInputObjectTypeJSON;
} else {
return FLEXArgInputObjectTypeAddress;
}
} else {
// Otherwise, see if we have more type information than just 'id'.
// If we do, make sure the encoding is something serializable to JSON.
// Properties and ivars keep more detailed type encoding information than method arguments.
if (strcmp(type, @encode(id)) != 0) {
BOOL isJSONSerializableType = NO;
// Parse class name out of the string,
// which is in the form `@"ClassName"`
Class cls = NSClassFromString(({
NSString *className = nil;
NSScanner *scan = [NSScanner scannerWithString:@(type)];
NSCharacterSet *allowed = [NSCharacterSet
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
];
// Skip over the @" then scan the name
if ([scan scanString:@"@\"" intoString:nil]) {
[scan scanCharactersFromSet:allowed intoString:&className];
}
className;
}));
// Note: we can't use @encode(NSString) here because that drops
// the class information and just goes to @encode(id).
NSArray<Class> *jsonTypes = @[
[NSString class],
[NSNumber class],
[NSArray class],
[NSDictionary class],
];
// Look for matching types
for (Class jsonClass in jsonTypes) {
if ([cls isSubclassOfClass:jsonClass]) {
isJSONSerializableType = YES;
break;
}
}
if (isJSONSerializableType) {
return FLEXArgInputObjectTypeJSON;
} else {
return FLEXArgInputObjectTypeAddress;
}
} else {
return FLEXArgInputObjectTypeAddress;
}
}
}
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/28/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputTextView.h"
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/28/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputStringView.h"
@@ -11,35 +11,119 @@
@implementation FLEXArgumentInputStringView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeLarge;
FLEXTypeEncoding type = typeEncoding[0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = typeEncoding[1];
}
// Selectors don't need a multi-line text box
if (type == FLEXTypeEncodingSelector) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
} else {
self.targetSize = FLEXArgumentInputViewSizeLarge;
}
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = inputValue;
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSString class]]) {
self.inputTextView.text = inputValue;
} else if ([inputValue isKindOfClass:[NSValue class]]) {
NSValue *value = (id)inputValue;
NSParameterAssert(strlen(value.objCType) == 1);
// C-String or SEL from NSValue
FLEXTypeEncoding type = value.objCType[0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = value.objCType[1];
}
if (type == FLEXTypeEncodingCString) {
self.inputTextView.text = @((const char *)value.pointerValue);
} else if (type == FLEXTypeEncodingSelector) {
self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue);
}
}
}
- (id)inputValue
{
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
- (id)inputValue {
NSString *text = self.inputTextView.text;
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
// but we accept that tradeoff in exchange for not having to type quotes for every string.
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
if (!text.length) {
return nil;
}
// Case: C-strings and SELs
if (self.typeEncoding.length <= 2) {
FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = [self.typeEncoding characterAtIndex:1];
}
if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) {
const char *encoding = self.typeEncoding.UTF8String;
SEL selector = NSSelectorFromString(text);
return [NSValue valueWithBytes:&selector objCType:encoding];
}
}
// Case: NSStrings
return self.inputTextView.text.copy;
}
// TODO: Support using object address for strings, as in the object arg view.
#pragma mark -
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
unsigned long len = strlen(type);
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
BOOL supported = type && strcmp(type, FLEXEncodeClass(NSString)) == 0;
supported = supported || (value && [value isKindOfClass:[NSString class]]);
return supported;
BOOL isConst = type[0] == FLEXTypeEncodingConst;
NSInteger i = isConst ? 1 : 0;
BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0;
BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString;
BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector;
BOOL valueIsString = [value isKindOfClass:[NSString class]];
BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString;
BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL;
BOOL valueIsNSValueWithCorrectType = NO;
if ([value isKindOfClass:[NSValue class]]) {
NSValue *v = (id)value;
len = strlen(v.objCType);
if (len == 1) {
FLEXTypeEncoding type = v.objCType[i];
if (type == FLEXTypeEncodingCString && typeIsCString) {
valueIsNSValueWithCorrectType = YES;
} else if (type == FLEXTypeEncodingSelector && typeIsSEL) {
valueIsNSValueWithCorrectType = YES;
}
}
}
if (!value && typeIsSupported) {
return YES;
}
if (typeIsString && valueIsString) {
return YES;
}
// Primitive strings can be input as NSStrings or NSValues
if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) {
return YES;
}
return NO;
}
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
@@ -3,37 +3,42 @@
// Flipboard
//
// Created by Ryan Olson on 6/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputStructView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXTypeEncodingParser.h"
@interface FLEXArgumentInputStructView ()
@property (nonatomic, strong) NSArray *argumentInputViews;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
@implementation FLEXArgumentInputStructView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
NSMutableArray *inputViews = [NSMutableArray array];
NSArray *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray new];
NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
inputView.backgroundColor = self.backgroundColor;
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
if (fieldIndex < [customTitles count]) {
inputView.title = [customTitles objectAtIndex:fieldIndex];
if (fieldIndex < customTitles.count) {
inputView.title = customTitles[fieldIndex];
} else {
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)",
structName, (unsigned long)fieldIndex, prettyTypeEncoding
];
}
[inputViews addObject:inputView];
@@ -47,34 +52,32 @@
#pragma mark - Superclass Overrides
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
inputView.backgroundColor = backgroundColor;
}
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSValue class]]) {
const char *structTypeEncoding = [inputValue objCType];
if (strcmp([self.typeEncoding UTF8String], structTypeEncoding) == 0) {
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
NSUInteger valueSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
NSGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL);
} @catch (NSException *exception) { }
if (valueSize > 0) {
if (FLEXGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL)) {
void *unboxedValue = malloc(valueSize);
[inputValue getValue:unboxedValue];
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
void *fieldPointer = unboxedValue + fieldOffset;
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
inputView.inputValue = (__bridge id)fieldPointer;
} else {
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
@@ -87,24 +90,23 @@
}
}
- (id)inputValue
{
- (id)inputValue {
NSValue *boxedStruct = nil;
const char *structTypeEncoding = [self.typeEncoding UTF8String];
const char *structTypeEncoding = self.typeEncoding.UTF8String;
NSUInteger structSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
NSGetSizeAndAlignment(structTypeEncoding, &structSize, NULL);
} @catch (NSException *exception) { }
if (structSize > 0) {
if (FLEXGetSizeAndAlignment(structTypeEncoding, &structSize, NULL)) {
void *unboxedStruct = malloc(structSize);
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
void *fieldPointer = unboxedStruct + fieldOffset;
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
// Object fields
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
} else {
@@ -123,8 +125,7 @@
return boxedStruct;
}
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
BOOL isFirstResponder = NO;
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
if ([inputView inputViewIsFirstResponder]) {
@@ -138,8 +139,7 @@
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
@@ -151,13 +151,11 @@
}
}
+ (CGFloat)verticalPaddingBetweenFields
{
+ (CGFloat)verticalPaddingBetweenFields {
return 10.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
@@ -174,20 +172,25 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return type && type[0] == '{';
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
if (type[0] == FLEXTypeEncodingStructBegin) {
return FLEXGetSizeAndAlignment(type, nil, nil);
}
return NO;
}
+ (NSArray *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
{
NSArray *customTitles = nil;
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
NSArray<NSString *> *customTitles = nil;
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
customTitles = @[@"CGPoint origin", @"CGSize size"];
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
customTitles = @[@"CGFloat x", @"CGFloat y"];
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
customTitles = @[@"CGFloat width", @"CGFloat height"];
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
@@ -203,6 +206,13 @@
customTitles = @[@"CGFloat a", @"CGFloat b",
@"CGFloat c", @"CGFloat d",
@"CGFloat tx", @"CGFloat ty"];
} else {
if (@available(iOS 11.0, *)) {
if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat leading",
@"CGFloat bottom", @"CGFloat trailing"];
}
}
}
return customTitles;
}
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 6/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
@@ -3,24 +3,23 @@
// Flipboard
//
// Created by Ryan Olson on 6/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXArgumentInputSwitchView ()
@property (nonatomic, strong) UISwitch *inputSwitch;
@property (nonatomic) UISwitch *inputSwitch;
@end
@implementation FLEXArgumentInputSwitchView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputSwitch = [[UISwitch alloc] init];
self.inputSwitch = [UISwitch new];
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
[self.inputSwitch sizeToFit];
[self addSubview:self.inputSwitch];
@@ -31,8 +30,7 @@
#pragma mark Input/Output
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
BOOL on = NO;
if ([inputValue isKindOfClass:[NSNumber class]]) {
NSNumber *number = (NSNumber *)inputValue;
@@ -46,30 +44,26 @@
self.inputSwitch.on = on;
}
- (id)inputValue
{
- (id)inputValue {
BOOL isOn = [self.inputSwitch isOn];
NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)];
return boxedBool;
}
- (void)switchValueDidChange:(id)sender
{
- (void)switchValueDidChange:(id)sender {
[self.delegate argumentInputViewValueDidChange:self];
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += self.inputSwitch.frame.size.height;
return fitSize;
@@ -78,10 +72,10 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// Only BOOLs. Current value is irrelevant.
return type && strcmp(type, @encode(BOOL)) == 0;
return strcmp(type, @encode(BOOL)) == 0;
}
@end
@@ -8,10 +8,11 @@
#import "FLEXArgumentInputView.h"
@interface FLEXArgumentInputTextView : FLEXArgumentInputView
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
// For subclass eyes only
@property (nonatomic, strong, readonly) UITextView *inputTextView;
@property (nonatomic, readonly) UITextView *inputTextView;
@property (nonatomic) NSString *inputPlaceholderText;
@end
@@ -6,105 +6,131 @@
//
//
#import "FLEXColor.h"
#import "FLEXArgumentInputTextView.h"
#import "FLEXUtility.h"
@interface FLEXArgumentInputTextView () <UITextViewDelegate>
@interface FLEXArgumentInputTextView ()
@property (nonatomic, strong) UITextView *inputTextView;
@property (nonatomic) UITextView *inputTextView;
@property (nonatomic) UILabel *placeholderLabel;
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
@end
@implementation FLEXArgumentInputTextView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView = [[UITextView alloc] init];
self.inputTextView = [UITextView new];
self.inputTextView.font = [[self class] inputFont];
self.inputTextView.backgroundColor = [UIColor whiteColor];
self.inputTextView.layer.borderColor = [[UIColor blackColor] CGColor];
self.inputTextView.layer.borderWidth = 1.0;
self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
self.inputTextView.layer.cornerRadius = 10.f;
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
if (@available(iOS 11, *)) {
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor;
}
self.placeholderLabel = [UILabel new];
self.placeholderLabel.font = self.inputTextView.font;
self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor;
self.placeholderLabel.numberOfLines = 0;
[self addSubview:self.inputTextView];
[self.inputTextView addSubview:self.placeholderLabel];
}
return self;
}
#pragma mark - private
#pragma mark - Private
- (UIToolbar*)createToolBar
{
- (UIToolbar *)createToolBar {
UIToolbar *toolBar = [UIToolbar new];
[toolBar sizeToFit];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(textViewDone)];
toolBar.items = @[spaceItem, doneItem];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil action:nil
];
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
target:self.inputTextView action:@selector(paste:)
];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self.inputTextView action:@selector(resignFirstResponder)
];
toolBar.items = @[spaceItem, pasteItem, doneItem];
return toolBar;
}
- (void)textViewDone
{
[self.inputTextView resignFirstResponder];
- (void)setInputPlaceholderText:(NSString *)placeholder {
self.placeholderLabel.text = placeholder;
if (placeholder.length) {
if (!self.inputTextView.text.length) {
self.placeholderLabel.hidden = NO;
} else {
self.placeholderLabel.hidden = YES;
}
} else {
self.placeholderLabel.hidden = YES;
}
[self setNeedsLayout];
}
#pragma mark - Text View Changes
- (void)textViewDidChange:(UITextView *)textView
{
[self.delegate argumentInputViewValueDidChange:self];
- (NSString *)inputPlaceholderText {
return self.placeholderLabel.text;
}
#pragma mark - Superclass Overrides
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return self.inputTextView.isFirstResponder;
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
// Placeholder label is positioned by insetting then origin
// by the content inset then the text container inset
CGSize s = self.inputTextView.frame.size;
self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height);
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
self.inputTextView.textContainerInset
);
}
- (NSUInteger)numberOfInputLines
{
NSUInteger numberOfInputLines = 0;
- (NSUInteger)numberOfInputLines {
switch (self.targetSize) {
case FLEXArgumentInputViewSizeDefault:
numberOfInputLines = 2;
break;
return 2;
case FLEXArgumentInputViewSizeSmall:
numberOfInputLines = 1;
break;
return 1;
case FLEXArgumentInputViewSizeLarge:
numberOfInputLines = 8;
break;
return 8;
}
return numberOfInputLines;
}
- (CGFloat)inputTextViewHeight
{
- (CGFloat)inputTextViewHeight {
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self inputTextViewHeight];
return fitSize;
@@ -113,9 +139,16 @@
#pragma mark - Class Helpers
+ (UIFont *)inputFont
{
return [FLEXUtility defaultFontOfSize:14.0];
+ (UIFont *)inputFont {
return [UIFont systemFontOfSize:14.0];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
}
@end
@@ -3,14 +3,17 @@
// Flipboard
//
// Created by Ryan Olson on 5/30/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
/// 2 lines, medium-sized
FLEXArgumentInputViewSizeDefault = 0,
/// One line
FLEXArgumentInputViewSizeSmall,
/// Several lines
FLEXArgumentInputViewSizeLarge
};
@@ -26,12 +29,13 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
/// To populate the filed with an initial value, set this property.
/// To reteive the value input by the user, access the property.
/// Primitive types and structs should/will be boxed in NSValue containers.
/// Concrete subclasses *must* override both the setter and getter for this property.
/// Concrete subclasses should override both the setter and getter for this property.
/// Subclasses can call super.inputValue to access a backing store for the value.
@property (nonatomic) id inputValue;
/// Setting this value to large will make some argument input views increase the size of their input field(s).
/// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
@property (nonatomic, assign) FLEXArgumentInputViewSize targetSize;
@property (nonatomic) FLEXArgumentInputViewSize targetSize;
/// Users of the input view can get delegate callbacks for incremental changes in user input.
@property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate;
@@ -47,8 +51,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
// For subclass eyes only
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) NSString *typeEncoding;
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic, readonly) NSString *typeEncoding;
@property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide;
@end
@@ -3,32 +3,31 @@
// Flipboard
//
// Created by Ryan Olson on 5/30/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
@interface FLEXArgumentInputView ()
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) NSString *typeEncoding;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) NSString *typeEncoding;
@end
@implementation FLEXArgumentInputView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithFrame:CGRectZero];
if (self) {
self.typeEncoding = @(typeEncoding);
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
}
return self;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
if (self.showsTitle) {
@@ -38,14 +37,12 @@
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.titleLabel.backgroundColor = backgroundColor;
}
- (void)setTitle:(NSString *)title
{
- (void)setTitle:(NSString *)title {
if (![_title isEqual:title]) {
_title = title;
self.titleLabel.text = title;
@@ -53,26 +50,22 @@
}
}
- (UILabel *)titleLabel
{
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel = [UILabel new];
_titleLabel.font = [[self class] titleFont];
_titleLabel.backgroundColor = self.backgroundColor;
_titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
_titleLabel.textColor = FLEXColor.primaryTextColor;
_titleLabel.numberOfLines = 0;
[self addSubview:_titleLabel];
}
return _titleLabel;
}
- (BOOL)showsTitle
{
return [self.title length] > 0;
- (BOOL)showsTitle {
return self.title.length > 0;
}
- (CGFloat)topInputFieldVerticalLayoutGuide
{
- (CGFloat)topInputFieldVerticalLayoutGuide {
CGFloat verticalLayoutGuide = 0;
if (self.showsTitle) {
CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height;
@@ -84,48 +77,32 @@
#pragma mark - Subclasses Can Override
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return NO;
}
- (void)setInputValue:(id)inputValue
{
// Subclasses should override.
}
- (id)inputValue
{
// Subclasses should override.
return nil;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
return NO;
}
#pragma mark - Class Helpers
+ (UIFont *)titleFont
{
return [FLEXUtility defaultFontOfSize:12.0];
+ (UIFont *)titleFont {
return [UIFont systemFontOfSize:12.0];
}
+ (CGFloat)titleBottomPadding
{
+ (CGFloat)titleBottomPadding {
return 4.0;
}
#pragma mark - Sizing
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = 0;
if ([self.title length] > 0) {
if (self.title.length > 0) {
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
height += ceil([self.titleLabel sizeThatFits:constrainSize].height);
height += [[self class] titleBottomPadding];
@@ -7,8 +7,7 @@
//
#import <Foundation/Foundation.h>
@class FLEXArgumentInputView;
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXArgumentInputViewFactory : NSObject
@@ -8,7 +8,7 @@
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputJSONObjectView.h"
#import "FLEXArgumentInputObjectView.h"
#import "FLEXArgumentInputNumberView.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXArgumentInputStructView.h"
@@ -17,55 +17,53 @@
#import "FLEXArgumentInputFontView.h"
#import "FLEXArgumentInputColorView.h"
#import "FLEXArgumentInputDateView.h"
#import "FLEXRuntimeUtility.h"
@implementation FLEXArgumentInputViewFactory
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
{
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding {
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
}
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
if (!subclass) {
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
// The unsupported view shows "nil" and does not allow user input.
subclass = [FLEXArgumentInputNotSupportedView class];
}
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding];
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset];
}
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
Class argumentInputViewSubclass = nil;
NSArray<Class> *inputViewClasses = @[[FLEXArgumentInputColorView class],
[FLEXArgumentInputFontView class],
[FLEXArgumentInputStringView class],
[FLEXArgumentInputStructView class],
[FLEXArgumentInputSwitchView class],
[FLEXArgumentInputDateView class],
[FLEXArgumentInputNumberView class],
[FLEXArgumentInputObjectView class]];
// Note that order is important here since multiple subclasses may support the same type.
// An example is the number subclass and the bool subclass for the type @encode(BOOL).
// Both work, but we'd prefer to use the bool subclass.
if ([FLEXArgumentInputColorView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputColorView class];
} else if ([FLEXArgumentInputFontView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputFontView class];
} else if ([FLEXArgumentInputStringView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputStringView class];
} else if ([FLEXArgumentInputStructView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputStructView class];
} else if ([FLEXArgumentInputSwitchView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputSwitchView class];
} else if ([FLEXArgumentInputDateView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputDateView class];
} else if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
} else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputJSONObjectView class];
for (Class inputViewClass in inputViewClasses) {
if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) {
argumentInputViewSubclass = inputViewClass;
break;
}
}
return argumentInputViewSubclass;
}
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
}
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXDefaultEditorViewController.h"
@@ -15,14 +15,13 @@
@interface FLEXDefaultEditorViewController ()
@property (nonatomic, readonly) NSUserDefaults *defaults;
@property (nonatomic, strong) NSString *key;
@property (nonatomic) NSString *key;
@end
@implementation FLEXDefaultEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key
{
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key {
self = [super initWithTarget:defaults];
if (self) {
self.key = key;
@@ -31,26 +30,26 @@
return self;
}
- (NSUserDefaults *)defaults
{
- (NSUserDefaults *)defaults {
return [self.target isKindOfClass:[NSUserDefaults class]] ? self.target : nil;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.fieldEditorView.fieldDescription = self.key;
id currentValue = [self.defaults objectForKey:self.key];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id) currentValue:currentValue];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = currentValue;
self.fieldEditorView.argumentInputViews = @[inputView];
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
id value = self.firstInputView.inputValue;
@@ -64,9 +63,17 @@
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
- (void)getterButtonPressed:(id)sender {
[super getterButtonPressed:sender];
id returnedObject = [self.defaults objectForKey:self.key];
[self exploreObjectOrPopViewController:returnedObject];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
return [FLEXArgumentInputViewFactory
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
];
}
@end
+4 -2
View File
@@ -3,16 +3,18 @@
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXArgumentInputView;
@interface FLEXFieldEditorView : UIView
@property (nonatomic, copy) NSString *targetDescription;
@property (nonatomic, copy) NSString *fieldDescription;
@property (nonatomic, strong) NSArray *argumentInputViews;
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
+23 -36
View File
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorView.h"
@@ -12,20 +12,19 @@
@interface FLEXFieldEditorView ()
@property (nonatomic, strong) UILabel *targetDescriptionLabel;
@property (nonatomic, strong) UIView *targetDescriptionDivider;
@property (nonatomic, strong) UILabel *fieldDescriptionLabel;
@property (nonatomic, strong) UIView *fieldDescriptionDivider;
@property (nonatomic) UILabel *targetDescriptionLabel;
@property (nonatomic) UIView *targetDescriptionDivider;
@property (nonatomic) UILabel *fieldDescriptionLabel;
@property (nonatomic) UIView *fieldDescriptionDivider;
@end
@implementation FLEXFieldEditorView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.targetDescriptionLabel = [[UILabel alloc] init];
self.targetDescriptionLabel = [UILabel new];
self.targetDescriptionLabel.numberOfLines = 0;
self.targetDescriptionLabel.font = [[self class] labelFont];
[self addSubview:self.targetDescriptionLabel];
@@ -33,7 +32,7 @@
self.targetDescriptionDivider = [[self class] dividerView];
[self addSubview:self.targetDescriptionDivider];
self.fieldDescriptionLabel = [[UILabel alloc] init];
self.fieldDescriptionLabel = [UILabel new];
self.fieldDescriptionLabel.numberOfLines = 0;
self.fieldDescriptionLabel.font = [[self class] labelFont];
[self addSubview:self.fieldDescriptionLabel];
@@ -44,8 +43,7 @@
return self;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat horizontalPadding = [[self class] horizontalPadding];
@@ -78,15 +76,13 @@
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.targetDescriptionLabel.backgroundColor = backgroundColor;
self.fieldDescriptionLabel.backgroundColor = backgroundColor;
}
- (void)setTargetDescription:(NSString *)targetDescription
{
- (void)setTargetDescription:(NSString *)targetDescription {
if (![_targetDescription isEqual:targetDescription]) {
_targetDescription = targetDescription;
self.targetDescriptionLabel.text = targetDescription;
@@ -94,8 +90,7 @@
}
}
- (void)setFieldDescription:(NSString *)fieldDescription
{
- (void)setFieldDescription:(NSString *)fieldDescription {
if (![_fieldDescription isEqual:fieldDescription]) {
_fieldDescription = fieldDescription;
self.fieldDescriptionLabel.text = fieldDescription;
@@ -103,8 +98,7 @@
}
}
- (void)setArgumentInputViews:(NSArray *)argumentInputViews
{
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
if (![_argumentInputViews isEqual:argumentInputViews]) {
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
@@ -121,40 +115,33 @@
}
}
+ (UIView *)dividerView
{
UIView *dividerView = [[UIView alloc] init];
+ (UIView *)dividerView {
UIView *dividerView = [UIView new];
dividerView.backgroundColor = [self dividerColor];
return dividerView;
}
+ (UIColor *)dividerColor
{
return [UIColor lightGrayColor];
+ (UIColor *)dividerColor {
return UIColor.lightGrayColor;
}
+ (CGFloat)horizontalPadding
{
+ (CGFloat)horizontalPadding {
return 10.0;
}
+ (CGFloat)verticalPadding
{
+ (CGFloat)verticalPadding {
return 20.0;
}
+ (UIFont *)labelFont
{
return [FLEXUtility defaultFontOfSize:14.0];
+ (UIFont *)labelFont {
return [UIFont systemFontOfSize:14.0];
}
+ (CGFloat)dividerLineHeight
{
+ (CGFloat)dividerLineHeight {
return 1.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat horizontalPadding = [[self class] horizontalPadding];
CGFloat verticalPadding = [[self class] verticalPadding];
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
+17 -16
View File
@@ -1,28 +1,29 @@
//
// FLEXFieldEditorViewController.h
// Flipboard
// FLEX
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXVariableEditorViewController.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
@class FLEXFieldEditorView;
@class FLEXArgumentInputView;
NS_ASSUME_NONNULL_BEGIN
@interface FLEXFieldEditorViewController : UIViewController
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
- (id)initWithTarget:(id)target;
/// @return nil if the property is readonly or if the type is unsupported
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property;
/// @return nil if the ivar type is unsupported
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar;
// Convenience accessor since many subclasses only use one input view
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
/// Subclasses can change the button title via the \c title property
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
// For subclass use only.
@property (nonatomic, strong, readonly) id target;
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
- (NSString *)titleForActionButton;
- (void)getterButtonPressed:(id)sender;
@end
NS_ASSUME_NONNULL_END
+129 -83
View File
@@ -1,117 +1,163 @@
//
// FLEXFieldEditorViewController.m
// Flipboard
// FLEX
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic) FLEXProperty *property;
@property (nonatomic) FLEXIvar *ivar;
@property (nonatomic, strong, readwrite) id target;
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
@property (nonatomic, readonly) id currentValue;
@property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding;
@property (nonatomic, readonly) NSString *fieldDescription;
@end
@implementation FLEXFieldEditorViewController
- (id)initWithTarget:(id)target
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.target = target;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
#pragma mark - Initialization
+ (instancetype)target:(id)target property:(FLEXProperty *)property {
id value = [property getValue:target];
if (![self canEditProperty:property onObject:target currentValue:value]) {
return nil;
}
return self;
FLEXFieldEditorViewController *editor = [self target:target];
editor.title = [@"Property: " stringByAppendingString:property.name];
editor.property = property;
return editor;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar {
FLEXFieldEditorViewController *editor = [self target:target];
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
editor.ivar = ivar;
return editor;
}
- (void)keyboardDidShow:(NSNotification *)notification
{
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
// Find the active input view and scroll to make sure it's visible.
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
if (argumentInputView.inputViewIsFirstResponder) {
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
break;
}
}
}
#pragma mark - Overrides
- (void)keyboardWillHide:(NSNotification *)notification
{
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = 0.0;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
self.navigationItem.rightBarButtonItem = self.setterButton;
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
// Create getter button
_getterButton = [[UIBarButtonItem alloc]
initWithTitle:@"Get"
style:UIBarButtonItemStyleDone
target:self
action:@selector(getterButtonPressed:)
];
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
];
// Configure input view
self.fieldEditorView.fieldDescription = self.fieldDescription;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
inputView.inputValue = self.currentValue;
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches; we mutate when the switch is flipped
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.actionButton.enabled = NO;
self.actionButton.title = @"Flip the switch to call the setter";
// Put getter button before setter button
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.actionButton, self.getterButton
];
}
}
- (void)viewWillLayoutSubviews
{
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
self.scrollView.contentSize = fieldEditorSize;
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
if (self.property) {
id userInputObject = self.firstInputView.inputValue;
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
SEL setterSelector = self.property.likelySetter;
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
[FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self];
sender = nil; // Don't pop back
}
} else {
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[self.ivar setValue:self.firstInputView.inputValue onObject:self.target];
}
// Go back after setting, but not for switches.
if (sender) {
[self.navigationController popViewControllerAnimated:YES];
} else {
self.firstInputView.inputValue = self.currentValue;
}
}
- (FLEXArgumentInputView *)firstInputView
{
return [[self.fieldEditorView argumentInputViews] firstObject];
}
- (void)actionButtonPressed:(id)sender
{
// Subclasses can override
- (void)getterButtonPressed:(id)sender {
[self.fieldEditorView endEditing:YES];
[self exploreObjectOrPopViewController:self.currentValue];
}
- (NSString *)titleForActionButton
{
// Subclasses can override.
return @"Set";
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
#pragma mark - Private
- (id)currentValue {
if (self.property) {
return [self.property getValue:self.target];
} else {
return [self.ivar getValue:self.target];
}
}
- (const FLEXTypeEncoding *)typeEncoding {
if (self.property) {
return self.property.attributes.typeEncoding.UTF8String;
} else {
return self.ivar.typeEncoding.UTF8String;
}
}
- (NSString *)fieldDescription {
if (self.property) {
return self.property.fullDescription;
} else {
return self.ivar.description;
}
}
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
return canEditType && [object respondsToSelector:property.likelySetter];
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -1,18 +0,0 @@
//
// FLEXIvarEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
@end
@@ -1,72 +0,0 @@
//
// FLEXIvarEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXIvarEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) Ivar ivar;
@end
@implementation FLEXIvarEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar
{
self = [super initWithTarget:target];
if (self) {
self.ivar = ivar;
self.title = @"Instance Variable";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:ivar_getTypeEncoding(self.ivar)];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches. Set the ivar when the switch toggles.
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -3,14 +3,14 @@
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import <objc/runtime.h>
#import "FLEXVariableEditorViewController.h"
#import "FLEXMethod.h"
@interface FLEXMethodCallingViewController : FLEXFieldEditorViewController
@interface FLEXMethodCallingViewController : FLEXVariableEditorViewController
- (id)initWithTarget:(id)target method:(Method)method;
+ (instancetype)target:(id)target method:(FLEXMethod *)method;
@end
@@ -3,7 +3,7 @@
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXMethodCallingViewController.h"
@@ -13,87 +13,93 @@
#import "FLEXObjectExplorerViewController.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXUtility.h"
@interface FLEXMethodCallingViewController ()
@property (nonatomic, assign) Method method;
@property (nonatomic) FLEXMethod *method;
@end
@implementation FLEXMethodCallingViewController
- (id)initWithTarget:(id)target method:(Method)method
{
+ (instancetype)target:(id)target method:(FLEXMethod *)method {
return [[self alloc] initWithTarget:target method:method];
}
- (id)initWithTarget:(id)target method:(FLEXMethod *)method {
NSParameterAssert(method.isInstanceMethod == !object_isClass(target));
self = [super initWithTarget:target];
if (self) {
self.method = method;
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: ";
self.title = [self.title stringByAppendingString:method.selectorString];
}
return self;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
NSMutableArray *argumentInputViews = [NSMutableArray array];
self.actionButton.title = @"Call";
// Configure field editor view
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
self.fieldEditorView.fieldDescription = [NSString stringWithFormat:
@"Signature:\n%@\n\nReturn Type:\n%s",
self.method.description, (char *)self.method.returnType
];
}
- (NSArray<FLEXArgumentInputView *> *)argumentInputViews {
Method method = self.method.objc_method;
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method];
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray new];
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
for (NSString *methodComponent in methodComponents) {
char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
char *argumentTypeEncoding = method_copyArgumentType(method, argumentIndex);
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding];
free(argumentTypeEncoding);
inputView.backgroundColor = self.view.backgroundColor;
inputView.title = methodComponent;
[argumentInputViews addObject:inputView];
argumentIndex++;
}
self.fieldEditorView.argumentInputViews = argumentInputViews;
return argumentInputViews;
}
- (BOOL)isClassMethod
{
return self.target && self.target == [self.target class];
}
- (NSString *)titleForActionButton
{
return @"Call";
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
NSMutableArray *arguments = [NSMutableArray array];
// Gather arguments
NSMutableArray *arguments = [NSMutableArray new];
for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
id argumentValue = inputView.inputValue;
if (!argumentValue) {
// Use NSNulls as placeholders in the array. They will be interpreted as nil arguments.
argumentValue = [NSNull null];
}
[arguments addObject:argumentValue];
// Use NSNull as a nil placeholder; it will be interpreted as nil
[arguments addObject:inputView.inputValue ?: NSNull.null];
}
// Call method
NSError *error = nil;
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
id returnValue = [FLEXRuntimeUtility
performSelector:self.method.selector
onObject:self.target
withArguments:arguments
error:&error
];
// Display return value or error
if (error) {
NSString *title = @"Method Call Failed";
NSString *message = [error localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
} else if (returnedObject) {
[FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self];
} else if (returnValue) {
// For non-nil (or void) return types, push an explorer view controller to display the returned object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
[self.navigationController pushViewController:explorerViewController animated:YES];
returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType];
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue];
[self.navigationController pushViewController:explorer animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
[self exploreObjectOrPopViewController:returnValue];
}
}
@@ -1,18 +0,0 @@
//
// FLEXPropertyEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property;
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
@end
@@ -1,94 +0,0 @@
//
// FLEXPropertyEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXPropertyEditorViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) objc_property_t property;
@end
@implementation FLEXPropertyEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property
{
self = [super initWithTarget:target];
if (self) {
self.property = property;
self.title = @"Property";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
self.setterButton.enabled = [[self class] canEditProperty:self.property currentValue:currentValue];
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:self.property] UTF8String];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches - just call the setter immediately after the switch toggles.
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
id userInputObject = self.firstInputView.inputValue;
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
NSString *title = @"Property Setter Failed";
NSString *message = [error localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
} else {
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
// Don't do this for simulated taps on the action button (i.e. from switch/BOOL editors). The experience is weird there.
if (sender) {
[self.navigationController popViewControllerAnimated:YES];
}
}
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
{
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
return canEditType && !isReadonly;
}
@end
@@ -0,0 +1,35 @@
//
// FLEXVariableEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXFieldEditorView;
@class FLEXArgumentInputView;
/// Provides a screen for editing or configuring one or more variables.
@interface FLEXVariableEditorViewController : UIViewController
+ (instancetype)target:(id)target;
- (id)initWithTarget:(id)target;
// Convenience accessor since many subclasses only use one input view
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
// For subclass use only.
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
/// Subclasses can change the button title via the button's \c title property
@property (nonatomic, readonly) UIBarButtonItem *actionButton;
- (void)actionButtonPressed:(id)sender;
/// Pushes an explorer view controller for the given object
/// or pops the current view controller.
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
@end
@@ -0,0 +1,137 @@
//
// FLEXVariableEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXVariableEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) id target;
@end
@implementation FLEXVariableEditorViewController
#pragma mark - Initialization
+ (instancetype)target:(id)target {
return [[self alloc] initWithTarget:target];
}
- (id)initWithTarget:(id)target {
self = [super init];
if (self) {
self.target = target;
[NSNotificationCenter.defaultCenter
addObserver:self selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification object:nil
];
[NSNotificationCenter.defaultCenter
addObserver:self selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification object:nil
];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
#pragma mark - UIViewController methods
- (void)keyboardDidShow:(NSNotification *)notification {
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
// Find the active input view and scroll to make sure it's visible.
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
if (argumentInputView.inputViewIsFirstResponder) {
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
break;
}
}
}
- (void)keyboardWillHide:(NSNotification *)notification {
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = 0.0;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = FLEXColor.scrollViewBackgroundColor;
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
_fieldEditorView = [FLEXFieldEditorView new];
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
_actionButton = [[UIBarButtonItem alloc]
initWithTitle:@"Set"
style:UIBarButtonItemStyleDone
target:self
action:@selector(actionButtonPressed:)
];
self.navigationController.toolbarHidden = NO;
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton];
}
- (void)viewWillLayoutSubviews {
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
self.scrollView.contentSize = fieldEditorSize;
}
#pragma mark - Public
- (FLEXArgumentInputView *)firstInputView {
return [self.fieldEditorView argumentInputViews].firstObject;
}
- (void)actionButtonPressed:(id)sender {
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
if (objectOrNil) {
// For non-nil (or void) return types, push an explorer view controller to display the object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
@@ -0,0 +1,19 @@
//
// FLEXBookmarkManager.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXBookmarkManager : NSObject
@property (nonatomic, readonly, class) NSMutableArray *bookmarks;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,25 @@
//
// FLEXBookmarkManager.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXBookmarkManager.h"
static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil;
@implementation FLEXBookmarkManager
+ (void)initialize {
if (self == [FLEXBookmarkManager class]) {
kFLEXBookmarkManagerBookmarks = [NSMutableArray new];
}
}
+ (NSMutableArray *)bookmarks {
return kFLEXBookmarkManagerBookmarks;
}
@end
@@ -0,0 +1,17 @@
//
// FLEXBookmarksViewController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXBookmarksViewController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,235 @@
//
// FLEXBookmarksViewController.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXBookmarksViewController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXBookmarkManager.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXColor.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXTableView.h"
@interface FLEXBookmarksViewController ()
@property (nonatomic, copy) NSArray *bookmarks;
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
@end
@implementation FLEXBookmarksViewController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.hidesBarsOnSwipe = NO;
self.tableView.allowsMultipleSelectionDuringEditing = YES;
[self reloadData];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupDefaultBarItems];
}
#pragma mark - Private
- (void)reloadData {
// We assume the bookmarks aren't going to change out from under us, since
// presenting any other tool via keyboard shortcuts should dismiss us first
self.bookmarks = FLEXBookmarkManager.bookmarks;
self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)];
}
- (void)setupDefaultBarItems {
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
// Disable editing if no bookmarks available
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
}
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
UIBarButtonItem.flex_flexibleSpace,
// We use a non-system done item because we change its title dynamically
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
];
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
}
- (FLEXExplorerViewController *)corePresenter {
// We must be presented by a FLEXExplorerViewController, or presented
// by another view controller that was presented by FLEXExplorerViewController
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
presenter = (id)presenter.presentingViewController ?: presenter;
presenter = (id)presenter.presentingViewController ?: presenter;
NSAssert(
[presenter isKindOfClass:[FLEXExplorerViewController class]],
@"The bookmarks view controller expects to be presented by the explorer controller"
);
return presenter;
}
#pragma mark Button Actions
- (void)dismissAnimated {
[self dismissAnimated:nil];
}
- (void)dismissAnimated:(id)selectedObject {
if (selectedObject) {
UIViewController *explorer = [FLEXObjectExplorerFactory
explorerViewControllerForObject:selectedObject
];
if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) {
// I am presented on an existing navigation stack, so
// dismiss myself and push the bookmark there
UINavigationController *presenter = (id)self.presentingViewController;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter pushViewController:explorer animated:YES];
}];
} else {
// Dismiss myself and present explorer
UIViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:[FLEXNavigationController
withRootViewController:explorer
] animated:YES completion:nil];
}];
}
} else {
// Just dismiss myself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)toggleEditing {
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
self.editing = !self.editing;
if (self.isEditing) {
[self setupEditingBarItems];
} else {
[self setupDefaultBarItems];
// Get index set of bookmarks to close
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
for (NSIndexPath *ip in selected) {
[indexes addIndex:ip.row];
}
if (selected.count) {
// Close bookmarks and update data source
[FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes];
[self reloadData];
// Remove deleted rows
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.bookmarks.count;
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self closeAll];
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)closeAll {
NSInteger rowCount = self.bookmarks.count;
// Close bookmarks and update data source
[FLEXBookmarkManager.bookmarks removeAllObjects];
[self reloadData];
// Delete rows from table view
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
return [NSIndexPath indexPathForRow:row inSection:0];
}];
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.bookmarks.count;
}
- (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
id object = self.bookmarks[indexPath.row];
cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.editing) {
// Case: editing with multi-select
self.toolbarItems.lastObject.title = @"Remove Selected";
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
} else {
// Case: selected a bookmark
[self dismissAnimated:self.bookmarks[indexPath.row]];
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(self.editing);
if (tableView.indexPathsForSelectedRows.count == 0) {
self.toolbarItems.lastObject.title = @"Done";
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)table
commitEditingStyle:(UITableViewCellEditingStyle)edit
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
// Remove bookmark and update data source
[FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row];
[self reloadData];
// Delete row from table view
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
@end
@@ -0,0 +1,51 @@
//
// FLEXExplorerViewController.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXExplorerToolbar.h"
@class FLEXWindow;
@protocol FLEXExplorerViewControllerDelegate;
/// A view controller that manages the FLEX toolbar.
@interface FLEXExplorerViewController : UIViewController
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
@property (nonatomic, readonly) BOOL wantsWindowToBecomeKey;
@property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar;
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
// Keyboard shortcut helpers
- (void)toggleSelectTool;
- (void)toggleMoveTool;
- (void)toggleViewsTool;
- (void)toggleMenuTool;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleDownArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleUpArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleRightArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleLeftArrowKeyPressed;
@end
#pragma mark -
@protocol FLEXExplorerViewControllerDelegate <NSObject>
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
@end
@@ -0,0 +1,996 @@
//
// FLEXExplorerViewController.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXExplorerViewController.h"
#import "FLEXExplorerToolbarItem.h"
#import "FLEXUtility.h"
#import "FLEXWindow.h"
#import "FLEXTabList.h"
#import "FLEXNavigationController.h"
#import "FLEXHierarchyViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXTabsViewController.h"
#import "FLEXWindowManagerController.h"
#import "FLEXViewControllersViewController.h"
#import "NSUserDefaults+FLEX.h"
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
FLEXExplorerModeDefault,
FLEXExplorerModeSelect,
FLEXExplorerModeMove
};
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
/// Tracks the currently active tool/mode
@property (nonatomic) FLEXExplorerMode currentMode;
/// Gesture recognizer for dragging a view in move mode
@property (nonatomic) UIPanGestureRecognizer *movePanGR;
/// Gesture recognizer for showing additional details on the selected view
@property (nonatomic) UITapGestureRecognizer *detailsTapGR;
/// Only valid while a move pan gesture is in progress.
@property (nonatomic) CGRect selectedViewFrameBeforeDragging;
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
/// 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;
/// The actual views at the selection point with the deepest view last.
@property (nonatomic) NSArray<UIView *> *viewsAtTapPoint;
/// The view that we're currently highlighting with an overlay and displaying details for.
@property (nonatomic) UIView *selectedView;
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic) UIView *selectedViewOverlay;
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
/// Used to preserve the target app's UIMenuController items.
@property (nonatomic) NSArray<UIMenuItem *> *appMenuItems;
@end
@implementation FLEXExplorerViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.observedViews = [NSMutableSet new];
}
return self;
}
- (void)dealloc {
for (UIView *view in _observedViews) {
[self stopObservingView:view];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Toolbar
_explorerToolbar = [FLEXExplorerToolbar new];
// Start the toolbar off below any bars that may be at the top of the view.
CGFloat toolbarOriginY = NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin;
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
)];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
)];
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin |
UIViewAutoresizingFlexibleTopMargin;
[self.view addSubview:self.explorerToolbar];
[self setupToolbarActions];
[self setupToolbarGestures];
// View selection
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSelectionTap:)
];
[self.view addGestureRecognizer:selectionTapGR];
// View moving
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
[self.view addGestureRecognizer:self.movePanGR];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateButtonStates];
}
#pragma mark - Rotation
- (UIViewController *)viewControllerForRotationAndOrientation {
UIViewController *viewController = FLEXUtility.appKeyWindow.rootViewController;
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
NSString *viewControllerSelectorString = [@[
@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"
] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
viewController = [viewController valueForKey:viewControllerSelectorString];
}
return viewController;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
// Commenting this out until I can figure out a better way to solve this
// if (self.window.isKeyWindow) {
// [self.window resignKeyWindow];
// }
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
// The UIViewController docs state that this method must not return zero.
// If we weren't able to get a valid value for the supported interface
// orientations, default to all supported.
if (supportedOrientations == 0) {
supportedOrientations = UIInterfaceOrientationMaskAll;
}
return supportedOrientations;
}
- (BOOL)shouldAutorotate {
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
}
return shouldAutorotate;
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
}
}
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}];
}
#pragma mark - Setter Overrides
- (void)setSelectedView:(UIView *)selectedView {
if (![_selectedView isEqual:selectedView]) {
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
[self stopObservingView:_selectedView];
}
_selectedView = selectedView;
[self beginObservingView:selectedView];
// Update the toolbar and selected overlay
self.explorerToolbar.selectedViewDescription = [FLEXUtility
descriptionForView:selectedView includingFrame:YES
];
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility
consistentRandomColorForObject:selectedView
];
if (selectedView) {
if (!self.selectedViewOverlay) {
self.selectedViewOverlay = [UIView new];
[self.view addSubview:self.selectedViewOverlay];
self.selectedViewOverlay.layer.borderWidth = 1.0;
}
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
// Make sure the selected overlay is in front of all the other subviews
// except the toolbar, which should always stay on top.
[self.view bringSubviewToFront:self.selectedViewOverlay];
[self.view bringSubviewToFront:self.explorerToolbar];
} else {
[self.selectedViewOverlay removeFromSuperview];
self.selectedViewOverlay = nil;
}
// Some of the button states depend on whether we have a selected view.
[self updateButtonStates];
}
}
- (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint {
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
for (UIView *view in _viewsAtTapPoint) {
if (view != self.selectedView) {
[self stopObservingView:view];
}
}
_viewsAtTapPoint = viewsAtTapPoint;
for (UIView *view in viewsAtTapPoint) {
[self beginObservingView:view];
}
}
}
- (void)setCurrentMode:(FLEXExplorerMode)currentMode {
if (_currentMode != currentMode) {
_currentMode = currentMode;
switch (currentMode) {
case FLEXExplorerModeDefault:
[self removeAndClearOutlineViews];
self.viewsAtTapPoint = nil;
self.selectedView = nil;
break;
case FLEXExplorerModeSelect:
// Make sure the outline views are unhidden in case we came from the move mode.
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = NO;
}
break;
case FLEXExplorerModeMove:
// Hide all the outline views to focus on the selected view,
// which is the only one that will move.
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = YES;
}
break;
}
self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
[self updateButtonStates];
}
}
#pragma mark - View Tracking
- (void)beginObservingView:(UIView *)view {
// Bail if we're already observing this view or if there's nothing to observe.
if (!view || [self.observedViews containsObject:view]) {
return;
}
for (NSString *keyPath in self.viewKeyPathsToTrack) {
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
}
[self.observedViews addObject:view];
}
- (void)stopObservingView:(UIView *)view {
if (!view) {
return;
}
for (NSString *keyPath in self.viewKeyPathsToTrack) {
[view removeObserver:self forKeyPath:keyPath];
}
[self.observedViews removeObject:view];
}
- (NSArray<NSString *> *)viewKeyPathsToTrack {
static NSArray<NSString *> *trackedViewKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
trackedViewKeyPaths = @[frameKeyPath];
});
return trackedViewKeyPaths;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *, id> *)change
context:(void *)context {
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
}
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object {
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
if (indexOfView != NSNotFound) {
UIView *view = self.viewsAtTapPoint[indexOfView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outline = self.outlineViewsForVisibleViews[key];
if (outline) {
outline.frame = [self frameInLocalCoordinatesForView:view];
}
}
if (object == self.selectedView) {
// Update the selected view description since we show the frame value there.
self.explorerToolbar.selectedViewDescription = [FLEXUtility
descriptionForView:self.selectedView includingFrame:YES
];
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
}
}
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view {
// Convert to window coordinates since the view may be in a different window than our view
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
// Convert from the window to our view's coordinate space
return [self.view convertRect:frameInWindow fromView:nil];
}
#pragma mark - Toolbar Buttons
- (void)setupToolbarActions {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
NSDictionary<NSString *, FLEXExplorerToolbarItem *> *actionsToItems = @{
NSStringFromSelector(@selector(selectButtonTapped:)): toolbar.selectItem,
NSStringFromSelector(@selector(hierarchyButtonTapped:)): toolbar.hierarchyItem,
NSStringFromSelector(@selector(recentButtonTapped:)): toolbar.recentItem,
NSStringFromSelector(@selector(moveButtonTapped:)): toolbar.moveItem,
NSStringFromSelector(@selector(globalsButtonTapped:)): toolbar.globalsItem,
NSStringFromSelector(@selector(closeButtonTapped:)): toolbar.closeItem,
};
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXExplorerToolbarItem *item, BOOL *stop) {
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
}];
}
- (void)selectButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleSelectTool];
}
- (void)hierarchyButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleViewsTool];
}
- (UIWindow *)statusWindow {
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [UIApplication.sharedApplication valueForKey:statusBarString];
}
- (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
NSAssert(FLEXTabList.sharedList.activeTab, @"Must have active tab");
[self presentViewController:FLEXTabList.sharedList.activeTab animated:YES completion:nil];
}
- (void)moveButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleMoveTool];
}
- (void)globalsButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleMenuTool];
}
- (void)closeButtonTapped:(FLEXExplorerToolbarItem *)sender {
self.currentMode = FLEXExplorerModeDefault;
[self.delegate explorerViewControllerDidFinish:self];
}
- (void)updateButtonStates {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
toolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
// Move only enabled when an object is selected.
BOOL hasSelectedObject = self.selectedView != nil;
toolbar.moveItem.enabled = hasSelectedObject;
toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
// Recent only enabled when we have a last active tab
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
}
#pragma mark - Toolbar Dragging
- (void)setupToolbarGestures {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
// Pan gesture for dragging.
[toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarPanGesture:)
]];
// Tap gesture for hinting.
[toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
]];
// Tap gesture for showing additional details
self.detailsTapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
// Swipe gestures for selecting deeper / higher views at a point
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer 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];
// Long press gesture to present tabs manager
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
]];
// Long press gesture to present window manager
[toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
]];
// Long press gesture to present view controllers at tap
[toolbar.hierarchyItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarShowViewControllersGesture:)
]];
}
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
[self updateToolbarPositionWithDragGesture:panGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateToolbarPositionWithDragGesture:panGR];
break;
default:
break;
}
}
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR {
CGPoint translation = [panGR translationInView:self.view];
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
newToolbarFrame.origin.y += translation.y;
[self updateToolbarPositionWithUnconstrainedFrame:newToolbarFrame];
}
- (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame {
CGRect safeArea = [self viewSafeArea];
// We only constrain the Y-axis because we want the toolbar
// to handle the X-axis safeArea layout by itself
CGFloat minY = CGRectGetMinY(safeArea);
CGFloat maxY = CGRectGetMaxY(safeArea) - unconstrainedFrame.size.height;
if (unconstrainedFrame.origin.y < minY) {
unconstrainedFrame.origin.y = minY;
} else if (unconstrainedFrame.origin.y > maxY) {
unconstrainedFrame.origin.y = maxY;
}
self.explorerToolbar.frame = unconstrainedFrame;
NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin = unconstrainedFrame.origin.y;
}
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR {
// Bounce the toolbar to indicate that it is draggable.
// TODO: make it bouncier.
if (tapGR.state == UIGestureRecognizerStateRecognized) {
CGRect originalToolbarFrame = self.explorerToolbar.frame;
const NSTimeInterval kHalfwayDuration = 0.2;
const CGFloat kVerticalOffset = 30.0;
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
CGRect newToolbarFrame = self.explorerToolbar.frame;
newToolbarFrame.origin.y += kVerticalOffset;
self.explorerToolbar.frame = newToolbarFrame;
} completion:^(BOOL finished) {
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.explorerToolbar.frame = originalToolbarFrame;
} completion:nil];
}];
}
}
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
[self presentViewController:
[FLEXNavigationController withRootViewController:topStackVC]
animated:YES completion:nil];
}
}
- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
// Don't use FLEXNavigationController because the tab viewer itself is not a tab
[super presentViewController:[[UINavigationController alloc]
initWithRootViewController:[FLEXTabsViewController new]
] animated:YES completion:nil];
}
}
- (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
[super presentViewController:[FLEXNavigationController
withRootViewController:[FLEXWindowManagerController new]
] animated:YES completion:nil];
}
}
- (void)handleToolbarShowViewControllersGesture:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan && self.viewsAtTapPoint.count) {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
UIViewController *list = [FLEXViewControllersViewController
controllersForViews:self.viewsAtTapPoint
];
[self presentViewController:
[FLEXNavigationController withRootViewController:list
] animated:YES completion:nil];
}
}
#pragma mark - View Selection
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR {
// Only if we're in selection mode
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
// Note that [tapGR locationInView:nil] is broken in iOS 8,
// so we have to do a two step conversion to window coordinates.
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
CGPoint tapPointInView = [tapGR locationInView:self.view];
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
}
}
- (void)handleChangeViewAtPointGesture:(UISwipeGestureRecognizer *)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)];
break;
default:
break;
}
}
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
[self removeAndClearOutlineViews];
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
// For outlined views and the selected view, only use visible views.
// Outlining hidden views adds clutter and makes the selection behavior confusing.
NSArray<UIView *> *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary new];
for (UIView *view in visibleViewsAtTapPoint) {
UIView *outlineView = [self outlineViewForView:view];
[self.view addSubview:outlineView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
[newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
}
self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
// Make sure the explorer toolbar doesn't end up behind the newly added outline views.
[self.view bringSubviewToFront:self.explorerToolbar];
[self updateButtonStates];
}
- (UIView *)outlineViewForView:(UIView *)view {
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
outlineView.backgroundColor = UIColor.clearColor;
outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
outlineView.layer.borderWidth = 1.0;
return outlineView;
}
- (void)removeAndClearOutlineViews {
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
[outlineView removeFromSuperview];
}
self.outlineViewsForVisibleViews = nil;
}
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden {
NSMutableArray<UIView *> *views = [NSMutableArray new];
for (UIWindow *window in FLEXUtility.allWindows) {
// Don't include the explorer's own window or subviews.
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
[views addObject:window];
[views addObjectsFromArray:[self
recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden
]];
}
}
return views;
}
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow {
// Select in the window that would handle the touch, but don't just use the result of
// hitTest:withEvent: so we can still select views with interaction disabled.
// Default to the the application's key window if none of the windows want the touch.
UIWindow *windowForSelection = UIApplication.sharedApplication.keyWindow;
for (UIWindow *window in FLEXUtility.allWindows.reverseObjectEnumerator) {
// Ignore the explorer's own window.
if (window != self.view.window) {
if ([window hitTest:tapPointInWindow withEvent:nil]) {
windowForSelection = window;
break;
}
}
}
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
}
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView
inView:(UIView *)view
skipHiddenViews:(BOOL)skipHidden {
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray new];
for (UIView *subview in view.subviews) {
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
if (skipHidden && isHidden) {
continue;
}
BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
if (subviewContainsPoint) {
[subviewsAtPoint addObject:subview];
}
// If this view doesn't clip to its bounds, we need to check its subviews even if it
// doesn't contain the selection point. They may be visible and contain the selection point.
if (subviewContainsPoint || !subview.clipsToBounds) {
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
[subviewsAtPoint addObjectsFromArray:[self
recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden
]];
}
}
return subviewsAtPoint;
}
#pragma mark - Selected View Moving
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR {
switch (movePanGR.state) {
case UIGestureRecognizerStateBegan:
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
[self updateSelectedViewPositionWithDragGesture:movePanGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateSelectedViewPositionWithDragGesture:movePanGR];
break;
default:
break;
}
}
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR {
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
self.selectedView.frame = newSelectedViewFrame;
}
#pragma mark - Safe Area Handling
- (CGRect)viewSafeArea {
CGRect safeArea = self.view.bounds;
if (@available(iOS 11.0, *)) {
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
}
return safeArea;
}
- (void)viewSafeAreaInsetsDidChange {
if (@available(iOS 11.0, *)) {
[super viewSafeAreaInsetsDidChange];
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
)];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
CGRectGetMinX(self.explorerToolbar.frame),
CGRectGetMinY(self.explorerToolbar.frame),
toolbarSize.width,
toolbarSize.height)
];
}
}
#pragma mark - Touch Handling
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
BOOL shouldReceiveTouch = NO;
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
// Always if it's on the toolbar
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
shouldReceiveTouch = YES;
}
// Always if we're in selection mode
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
shouldReceiveTouch = YES;
}
// Always in move mode too
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
shouldReceiveTouch = YES;
}
// Always if we have a modal presented
if (!shouldReceiveTouch && self.presentedViewController) {
shouldReceiveTouch = YES;
}
return shouldReceiveTouch;
}
#pragma mark - FLEXHierarchyDelegate
- (void)viewHierarchyDidDismiss:(UIView *)selectedView {
// Note that we need to wait until the view controller is dismissed to calculate the frame
// of the outline view, otherwise the coordinate conversion doesn't give the correct result.
[self toggleViewsToolWithCompletion:^{
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
// then clear out the tap point array and remove all the outline views.
if (![self.viewsAtTapPoint containsObject:selectedView]) {
self.viewsAtTapPoint = nil;
[self removeAndClearOutlineViews];
}
// If we now have a selected view and we didn't have one previously, go to "select" mode.
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
self.currentMode = FLEXExplorerModeSelect;
}
// The selected view setter will also update the selected view overlay appropriately.
self.selectedView = selectedView;
}];
}
#pragma mark - Modal Presentation and Window Management
- (void)presentViewController:(UIViewController *)toPresent
animated:(BOOL)animated
completion:(void (^)(void))completion {
// Make our window key to correctly handle input.
[self.view.window makeKeyWindow];
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
if (!@available(iOS 13, *)) {
[self statusWindow].windowLevel = self.view.window.windowLevel + 1.0;
}
// Back up and replace the UIMenuController items
// Edit: no longer replacing the items, but still backing them
// up in case we start replacing them again in the future
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
// Show the view controller
[super presentViewController:toPresent animated:animated completion:completion];
}
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
UIWindow *appWindow = self.window.previousKeyWindow;
[appWindow makeKeyWindow];
[appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
// Restore previous UIMenuController items
// Back up and replace the UIMenuController items
UIMenuController.sharedMenuController.menuItems = self.appMenuItems;
[UIMenuController.sharedMenuController update];
self.appMenuItems = nil;
// Restore the status bar window's normal window level.
// We want it above FLEX while a modal is presented for
// scroll to top, but below FLEX otherwise for exploration.
[self statusWindow].windowLevel = UIWindowLevelStatusBar;
[self updateButtonStates];
[super dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
return self.window.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void(^)(void))completion {
if (self.presentedViewController) {
[self dismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (FLEXWindow *)window {
return (id)self.view.window;
}
#pragma mark - Keyboard Shortcut Helpers
- (void)toggleSelectTool {
if (self.currentMode == FLEXExplorerModeSelect) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeSelect;
}
}
- (void)toggleMoveTool {
if (self.currentMode == FLEXExplorerModeMove) {
self.currentMode = FLEXExplorerModeSelect;
} else if (self.currentMode == FLEXExplorerModeSelect && self.selectedView) {
self.currentMode = FLEXExplorerModeMove;
}
}
- (void)toggleViewsTool {
[self toggleViewsToolWithCompletion:nil];
}
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion {
[self toggleToolWithViewControllerProvider:^UINavigationController *{
if (self.selectedView) {
return [FLEXHierarchyViewController
delegate:self
viewsAtTap:self.viewsAtTapPoint
selectedView:self.selectedView
];
} else {
return [FLEXHierarchyViewController delegate:self];
}
} completion:^{
if (completion) {
completion();
}
}];
}
- (void)toggleMenuTool {
[self toggleToolWithViewControllerProvider:^UINavigationController *{
return [FLEXNavigationController withRootViewController:[FLEXGlobalsViewController new]];
} completion:nil];
}
- (BOOL)handleDownArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
}
} else {
return NO;
}
return YES;
}
- (BOOL)handleUpArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
} else {
return NO;
}
return YES;
}
- (BOOL)handleRightArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
return YES;
}
return NO;
}
- (BOOL)handleLeftArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
return YES;
}
return NO;
}
@end
@@ -0,0 +1,19 @@
//
// FLEXViewControllersViewController.h
// FLEX
//
// Created by Tanner Bennett on 2/13/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXViewControllersViewController : FLEXFilteringTableViewController
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,79 @@
//
// FLEXViewControllersViewController.m
// FLEX
//
// Created by Tanner Bennett on 2/13/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXViewControllersViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
@interface FLEXViewControllersViewController ()
@property (nonatomic, readonly) FLEXMutableListSection *section;
@property (nonatomic, readonly) NSArray<UIViewController *> *controllers;
@end
@implementation FLEXViewControllersViewController
@dynamic sections, allSections;
#pragma mark - Initialization
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views {
return [[self alloc] initWithViews:views];
}
- (id)initWithViews:(NSArray<UIView *> *)views {
NSParameterAssert(views.count);
self = [self initWithStyle:UITableViewStylePlain];
if (self) {
_controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) {
return [FLEXUtility viewControllerForView:view];
}];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"View Controllers at Tap";
self.showsSearchBar = YES;
[self disableToolbar];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
_section = [FLEXMutableListSection list:self.controllers
cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) {
cell.textLabel.text = [NSString
stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller
];
cell.detailTextLabel.text = controller.view.description;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
} filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) {
return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText];
}];
self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) {
[host.navigationController pushViewController:
[FLEXObjectExplorerFactory explorerViewControllerForObject:controller]
animated:YES];
};
self.section.customTitle = @"View Controllers";
return @[self.section];
}
#pragma mark - Private
- (void)dismissAnimated {
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
+29
View File
@@ -0,0 +1,29 @@
//
// FLEXWindow.h
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol FLEXWindowEventDelegate <NSObject>
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
- (BOOL)canBecomeKeyWindow;
@end
#pragma mark -
@interface FLEXWindow : UIWindow
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture input.
/// If we're just showing the toolbar, we want the main app's window to remain key
/// so that we don't interfere with input, status bar, etc.
@property (nonatomic, readonly) UIWindow *previousKeyWindow;
@end
@@ -3,19 +3,18 @@
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXWindow.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@implementation FLEXWindow
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
@@ -25,8 +24,7 @@
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
@@ -34,22 +32,29 @@
return pointInside;
}
- (BOOL)shouldAffectStatusBarAppearance
{
- (BOOL)shouldAffectStatusBarAppearance {
return [self isKeyWindow];
}
- (BOOL)canBecomeKeyWindow
{
- (BOOL)canBecomeKeyWindow {
return [self.eventDelegate canBecomeKeyWindow];
}
+ (void)initialize
{
- (void)makeKeyWindow {
_previousKeyWindow = FLEXUtility.appKeyWindow;
[super makeKeyWindow];
}
- (void)resignKeyWindow {
[super resignKeyWindow];
_previousKeyWindow = nil;
}
+ (void)initialize {
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
// Done at runtime with an obfuscated selector because it is private API. But you shoudn't ship this to the App Store anyways...
// Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways...
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
@@ -0,0 +1,17 @@
//
// FLEXWindowManagerController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXWindowManagerController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,302 @@
//
// FLEXWindowManagerController.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXWindowManagerController.h"
#import "FLEXManager+Private.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
@interface FLEXWindowManagerController ()
@property (nonatomic) UIWindow *keyWindow;
@property (nonatomic, copy) NSString *keyWindowSubtitle;
@property (nonatomic, copy) NSArray<UIWindow *> *windows;
@property (nonatomic, copy) NSArray<NSString *> *windowSubtitles;
@property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13));
@property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles;
@property (nonatomic, copy) NSArray<NSArray *> *sections;
@end
@implementation FLEXWindowManagerController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Windows";
if (@available(iOS 13, *)) {
self.title = @"Windows and Scenes";
}
[self disableToolbar];
[self reloadData];
}
#pragma mark - Private
- (void)reloadData {
self.keyWindow = UIApplication.sharedApplication.keyWindow;
self.windows = UIApplication.sharedApplication.windows;
self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]];
self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) {
return [NSString stringWithFormat:@"Level: %@ — Root: %@",
@(window.windowLevel), window.rootViewController
];
}];
if (@available(iOS 13, *)) {
self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects;
self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) {
return [self sceneDescription:scene];
}];
self.sections = @[@[self.keyWindow], self.windows, self.scenes];
} else {
self.sections = @[@[self.keyWindow], self.windows];
}
[self.tableView reloadData];
}
- (void)dismissAnimated {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
[self reloadData];
[self.tableView reloadData];
UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow;
UIWindowLevel maxLevel = 0;
for (UIWindow *window in UIApplication.sharedApplication.windows) {
if (window.windowLevel > maxLevel) {
maxLevel = window.windowLevel;
highestWindow = window;
}
}
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Keep Changes?");
make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below.");
make.button(@"Keep Changes").destructiveStyle();
make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self dismissAnimated];
});
make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) {
revertBlock();
[self reloadData];
[self.tableView reloadData];
});
} showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]];
}
- (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) {
NSString *state = [self stringFromSceneState:scene.activationState];
NSString *title = scene.title.length ? scene.title : nil;
NSString *suffix = nil;
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (id)scene;
suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window");
}
NSMutableString *description = state.mutableCopy;
if (title) {
[description appendFormat:@" — %@", title];
}
if (suffix) {
[description appendFormat:@" — %@", suffix];
}
return description.copy;
}
- (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) {
switch (state) {
case UISceneActivationStateUnattached:
return @"Unattached";
case UISceneActivationStateForegroundActive:
return @"Active";
case UISceneActivationStateForegroundInactive:
return @"Inactive";
case UISceneActivationStateBackground:
return @"Backgrounded";
}
return [NSString stringWithFormat:@"Unknown state: %@", @(state)];
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.sections[section].count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
switch (section) {
case 0: return @"Key Window";
case 1: return @"Windows";
case 2: return @"Connected Scenes";
}
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDetailButton;
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
UIWindow *window = nil;
NSString *subtitle = nil;
switch (indexPath.section) {
case 0:
window = self.keyWindow;
subtitle = self.keyWindowSubtitle;
break;
case 1:
window = self.windows[indexPath.row];
subtitle = self.windowSubtitles[indexPath.row];
break;
case 2:
if (@available(iOS 13, *)) {
UIScene *scene = self.scenes[indexPath.row];
cell.textLabel.text = scene.description;
cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row];
return cell;
}
}
cell.textLabel.text = window.description;
cell.detailTextLabel.text = [NSString
stringWithFormat:@"Level: %@ — Root: %@",
@((NSInteger)window.windowLevel), window.rootViewController.class
];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIWindow *window = nil;
NSString *subtitle = nil;
FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow;
id cancelHandler = ^{
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
};
switch (indexPath.section) {
case 0:
window = self.keyWindow;
subtitle = self.keyWindowSubtitle;
break;
case 1:
window = self.windows[indexPath.row];
subtitle = self.windowSubtitles[indexPath.row];
break;
case 2:
if (@available(iOS 13, *)) {
UIScene *scene = self.scenes[indexPath.row];
UIWindowScene *oldScene = flex.windowScene;
BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]];
BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(NSStringFromClass(scene.class));
if (isWindowScene) {
if (isFLEXScene) {
make.message(@"Already the FLEX window scene");
}
make.button(@"Set as FLEX Window Scene")
.handler(^(NSArray<NSString *> *strings) {
flex.windowScene = (id)scene;
[self showRevertOrDismissAlert:^{
flex.windowScene = oldScene;
}];
}).enabled(!isFLEXScene);
make.button(@"Cancel").cancelStyle();
} else {
make.message(@"Not a UIWindowScene");
make.button(@"Dismiss").cancelStyle().handler(cancelHandler);
}
} showFrom:self];
}
}
__block UIWindow *targetWindow = nil, *oldKeyWindow = nil;
__block UIWindowLevel oldLevel;
__block BOOL wasVisible;
subtitle = [subtitle stringByAppendingString:
@"\n\n1) Adjust the FLEX window level relative to this window,\n"
"2) adjust this window's level relative to the FLEX window,\n"
"3) set this window's level to a specific value, or\n"
"4) make this window the key window if it isn't already."
];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(NSStringFromClass(window.class)).message(subtitle);
make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = flex; oldLevel = flex.windowLevel;
flex.windowLevel = window.windowLevel + strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = window; oldLevel = window.windowLevel;
window.windowLevel = flex.windowLevel + strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = window; oldLevel = window.windowLevel;
window.windowLevel = strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) {
oldKeyWindow = UIApplication.sharedApplication.keyWindow;
wasVisible = window.hidden;
[window makeKeyAndVisible];
[self showRevertOrDismissAlert:^{
window.hidden = wasVisible;
[oldKeyWindow makeKeyWindow];
}];
}).enabled(!window.isKeyWindow && !window.hidden);
make.button(@"Cancel").cancelStyle().handler(cancelHandler);
make.textField(@"+/- window level, i.e. 5 or -10");
} showFrom:self];
}
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip {
[self.navigationController pushViewController:
[FLEXObjectExplorerFactory explorerViewControllerForObject:self.sections[ip.section][ip.row]]
animated:YES];
}
@end
@@ -0,0 +1,45 @@
//
// FLEXTabList.h
// FLEX
//
// Created by Tanner on 2/1/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTabList : NSObject
@property (nonatomic, readonly, class) FLEXTabList *sharedList;
@property (nonatomic, readonly, nullable) UINavigationController *activeTab;
@property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs;
/// Snapshots of each tab when they were last active.
@property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots;
/// \c NSNotFound if no tabs are present.
/// Setting this property changes the active tab to one of the already open tabs.
@property (nonatomic) NSInteger activeTabIndex;
/// Adds a new tab and sets the new tab as the active tab.
- (void)addTab:(UINavigationController *)newTab;
/// Closes the given tab. If this tab was the active tab,
/// the most recent tab before that becomes the active tab.
- (void)closeTab:(UINavigationController *)tab;
/// Closes a tab at the given index. If this tab was the active tab,
/// the most recent tab before that becomes the active tab.
- (void)closeTabAtIndex:(NSInteger)idx;
/// Closes all of the tabs at the given indexes. If the active tab
/// is included, the most recent still-open tab becomes the active tab.
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes;
/// A shortcut to close the active tab.
- (void)closeActiveTab;
/// A shortcut to close \e every tab.
- (void)closeAllTabs;
- (void)updateSnapshotForActiveTab;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,133 @@
//
// FLEXTabList.m
// FLEX
//
// Created by Tanner on 2/1/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTabList.h"
#import "FLEXUtility.h"
@interface FLEXTabList () {
NSMutableArray *_openTabs;
NSMutableArray *_openTabSnapshots;
}
@end
#pragma mark -
@implementation FLEXTabList
#pragma mark Initialization
+ (FLEXTabList *)sharedList {
static FLEXTabList *sharedList = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedList = [self new];
});
return sharedList;
}
- (id)init {
self = [super init];
if (self) {
_openTabs = [NSMutableArray new];
_openTabSnapshots = [NSMutableArray new];
_activeTabIndex = NSNotFound;
}
return self;
}
#pragma mark Private
- (void)chooseNewActiveTab {
if (self.openTabs.count) {
self.activeTabIndex = self.openTabs.count - 1;
} else {
self.activeTabIndex = NSNotFound;
}
}
#pragma mark Public
- (void)setActiveTabIndex:(NSInteger)idx {
NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound);
if (_activeTabIndex == idx) return;
_activeTabIndex = idx;
_activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx];
}
- (void)addTab:(UINavigationController *)newTab {
NSParameterAssert(newTab);
// Update snapshot of the last active tab
if (self.activeTab) {
[self updateSnapshotForActiveTab];
}
// Add new tab and snapshot,
// update active tab and index
[_openTabs addObject:newTab];
[_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]];
_activeTab = newTab;
_activeTabIndex = self.openTabs.count - 1;
}
- (void)closeTab:(UINavigationController *)tab {
NSParameterAssert(tab);
NSParameterAssert([self.openTabs containsObject:tab]);
NSInteger idx = [self.openTabs indexOfObject:tab];
[self closeTabAtIndex:idx];
}
- (void)closeTabAtIndex:(NSInteger)idx {
NSParameterAssert(idx < self.openTabs.count);
// Remove old tab and snapshot
[_openTabs removeObjectAtIndex:idx];
[_openTabSnapshots removeObjectAtIndex:idx];
// Update active tab and index if needed
if (self.activeTabIndex == idx) {
[self chooseNewActiveTab];
}
}
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes {
// Remove old tabs and snapshot
[_openTabs removeObjectsAtIndexes:indexes];
[_openTabSnapshots removeObjectsAtIndexes:indexes];
// Update active tab and index if needed
if ([indexes containsIndex:self.activeTabIndex]) {
[self chooseNewActiveTab];
}
}
- (void)closeActiveTab {
[self closeTab:self.activeTab];
}
- (void)closeAllTabs {
// Remove tabs and snapshots
[_openTabs removeAllObjects];
[_openTabSnapshots removeAllObjects];
// Update active tab index
self.activeTabIndex = NSNotFound;
}
- (void)updateSnapshotForActiveTab {
if (self.activeTabIndex != NSNotFound) {
UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view];
[_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot];
}
}
@end
@@ -0,0 +1,13 @@
//
// FLEXTabsViewController.h
// FLEX
//
// Created by Tanner on 2/4/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
@interface FLEXTabsViewController : FLEXTableViewController
@end
@@ -0,0 +1,335 @@
//
// FLEXTabsViewController.m
// FLEX
//
// Created by Tanner on 2/4/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTabsViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXTabList.h"
#import "FLEXBookmarkManager.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXExplorerViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXBookmarksViewController.h"
@interface FLEXTabsViewController ()
@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
@property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots;
@property (nonatomic) NSInteger activeIndex;
@property (nonatomic) BOOL presentNewActiveTabOnDismiss;
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
@end
@implementation FLEXTabsViewController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Open Tabs";
self.navigationController.hidesBarsOnSwipe = NO;
self.tableView.allowsMultipleSelectionDuringEditing = YES;
[self reloadData:NO];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupDefaultBarItems];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Instead of updating the active snapshot before we present,
// we update it after we present to avoid pre-presenation latency
dispatch_async(dispatch_get_main_queue(), ^{
[FLEXTabList.sharedList updateSnapshotForActiveTab];
[self reloadData:NO];
[self.tableView reloadData];
});
}
#pragma mark - Private
/// @param trackActiveTabDelta whether to check if the active
/// tab changed and needs to be presented upon "Done" dismissal.
/// @return whether the active tab changed or not (if there are any tabs left)
- (BOOL)reloadData:(BOOL)trackActiveTabDelta {
BOOL activeTabDidChange = NO;
FLEXTabList *list = FLEXTabList.sharedList;
// Flag to enable check to determine whether
if (trackActiveTabDelta) {
NSInteger oldActiveIndex = self.activeIndex;
if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) {
self.presentNewActiveTabOnDismiss = YES;
activeTabDidChange = YES;
} else if (self.presentNewActiveTabOnDismiss) {
// If we had something to present before, now we don't
// (i.e. activeTabIndex == NSNotFound)
self.presentNewActiveTabOnDismiss = NO;
}
}
// We assume the tabs aren't going to change out from under us, since
// presenting any other tool via keyboard shortcuts should dismiss us first
self.openTabs = list.openTabs;
self.tabSnapshots = list.openTabSnapshots;
self.activeIndex = list.activeTabIndex;
return activeTabDidChange;
}
- (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged {
// Refresh the newly active tab row if needed
if (activeTabChanged) {
NSIndexPath *active = [NSIndexPath
indexPathForRow:self.activeIndex inSection:0
];
[self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone];
}
}
- (void)setupDefaultBarItems {
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
self.toolbarItems = @[
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed:)),
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
// Disable editing if no tabs available
self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
}
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
UIBarButtonItem.flex_flexibleSpace,
[UIBarButtonItem disabledSystemItem:UIBarButtonSystemItemAdd],
UIBarButtonItem.flex_flexibleSpace,
// We use a non-system done item because we change its title dynamically
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
];
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
}
- (FLEXExplorerViewController *)corePresenter {
// We must be presented by a FLEXExplorerViewController, or presented
// by another view controller that was presented by FLEXExplorerViewController
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
presenter = (id)presenter.presentingViewController ?: presenter;
NSAssert(
[presenter isKindOfClass:[FLEXExplorerViewController class]],
@"The tabs view controller expects to be presented by the explorer controller"
);
return presenter;
}
#pragma mark Button Actions
- (void)dismissAnimated {
if (self.presentNewActiveTabOnDismiss) {
// The active tab was closed so we need to present the new one
UIViewController *activeTab = FLEXTabList.sharedList.activeTab;
FLEXExplorerViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:activeTab animated:YES completion:nil];
}];
} else if (self.activeIndex == NSNotFound) {
// The only tab was closed, so dismiss everything
[self.corePresenter dismissViewControllerAnimated:YES completion:nil];
} else {
// Simple dismiss with the same active tab, only dismiss myself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)toggleEditing {
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
self.editing = !self.editing;
if (self.isEditing) {
[self setupEditingBarItems];
} else {
[self setupDefaultBarItems];
// Get index set of tabs to close
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
for (NSIndexPath *ip in selected) {
[indexes addIndex:ip.row];
}
if (selected.count) {
// Close tabs and update data source
[FLEXTabList.sharedList closeTabsAtIndexes:indexes];
BOOL activeTabChanged = [self reloadData:YES];
// Remove deleted rows
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
// Refresh the newly active tab row if needed
[self reloadActiveTabRowIfChanged:activeTabChanged];
}
}
}
- (void)addTabButtonPressed:(UIBarButtonItem *)sender {
if (FLEXBookmarkManager.bookmarks.count) {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"New Tab");
make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) {
[self addTabAndDismiss:[FLEXNavigationController
withRootViewController:[FLEXGlobalsViewController new]
]];
});
make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) {
[self presentViewController:[FLEXNavigationController
withRootViewController:[FLEXBookmarksViewController new]
] animated:YES completion:nil];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
} else {
// No bookmarks, just open the main menu
[self addTabAndDismiss:[FLEXNavigationController
withRootViewController:[FLEXGlobalsViewController new]
]];
}
}
- (void)addTabAndDismiss:(UINavigationController *)newTab {
FLEXExplorerViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:newTab animated:YES completion:nil];
}];
}
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.openTabs.count;
NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self closeAll];
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)closeAll {
NSInteger rowCount = self.openTabs.count;
// Close tabs and update data source
[FLEXTabList.sharedList closeAllTabs];
[self reloadData:YES];
// Delete rows from table view
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
return [NSIndexPath indexPathForRow:row inSection:0];
}];
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.openTabs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
UINavigationController *tab = self.openTabs[indexPath.row];
cell.imageView.image = self.tabSnapshots[indexPath.row];
cell.textLabel.text = tab.topViewController.title;
cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page");
if (!cell.tag) {
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
cell.tag = 1;
}
if (indexPath.row == self.activeIndex) {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
}
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.editing) {
// Case: editing with multi-select
self.toolbarItems.lastObject.title = @"Close Selected";
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
} else {
if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
// Case: selected the already active tab
[self dismissAnimated];
} else {
// Case: selected a different tab,
// or selected a tab when presented from the FLEX toolbar
FLEXTabList.sharedList.activeTabIndex = indexPath.row;
self.presentNewActiveTabOnDismiss = YES;
[self dismissAnimated];
}
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(self.editing);
if (tableView.indexPathsForSelectedRows.count == 0) {
self.toolbarItems.lastObject.title = @"Done";
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)table
commitEditingStyle:(UITableViewCellEditingStyle)edit
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
// Close tab and update data source
[FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]];
BOOL activeTabChanged = [self reloadData:YES];
// Delete row from table view
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// Refresh the newly active tab row if needed
[self reloadActiveTabRowIfChanged:activeTabChanged];
}
@end
@@ -1,49 +0,0 @@
//
// FLEXExplorerToolbar.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXToolbarItem;
@interface FLEXExplorerToolbar : UIView
/// Toolbar item for selecting views.
/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
@property (nonatomic, strong, readonly) FLEXToolbarItem *selectItem;
/// Toolbar item for presenting a list with the view hierarchy.
/// Users of the toolbar can configure the enabled state and event targets/actions.
@property (nonatomic, strong, readonly) FLEXToolbarItem *hierarchyItem;
/// Toolbar item for moving views.
/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
@property (nonatomic, strong, readonly) FLEXToolbarItem *moveItem;
/// Toolbar item for inspecting details of the selected view.
/// Users of the toolbar can configure the enabled state and event targets/actions.
@property (nonatomic, strong, readonly) FLEXToolbarItem *globalsItem;
/// Toolbar item for hiding the explorer.
/// Users of the toolbar can configure the event targets/actions.
@property (nonatomic, strong, readonly) FLEXToolbarItem *closeItem;
/// A view for moving the entire toolbar.
/// Users of the toolbar can attach a pan gesture recognizer to decide how to reposition the toolbar.
@property (nonatomic, strong, readonly) UIView *dragHandle;
/// A color matching the overlay on color on the selected view.
@property (nonatomic, strong) UIColor *selectedViewOverlayColor;
/// Description text for the selected view displayed below the toolbar items.
@property (nonatomic, copy) NSString *selectedViewDescription;
/// Area where details of the selected view are shown
/// Users of the toolbar can attach a tap gesture recognizer to show additional details.
@property (nonatomic, strong, readonly) UIView *selectedViewDescriptionContainer;
@end
@@ -1,226 +0,0 @@
//
// FLEXExplorerToolbar.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXExplorerToolbar.h"
#import "FLEXToolbarItem.h"
#import "FLEXResources.h"
#import "FLEXUtility.h"
@interface FLEXExplorerToolbar ()
@property (nonatomic, strong, readwrite) FLEXToolbarItem *selectItem;
@property (nonatomic, strong, readwrite) FLEXToolbarItem *moveItem;
@property (nonatomic, strong, readwrite) FLEXToolbarItem *globalsItem;
@property (nonatomic, strong, readwrite) FLEXToolbarItem *closeItem;
@property (nonatomic, strong, readwrite) FLEXToolbarItem *hierarchyItem;
@property (nonatomic, strong, readwrite) UIView *dragHandle;
@property (nonatomic, strong) UIImageView *dragHandleImageView;
@property (nonatomic, strong) NSArray *toolbarItems;
@property (nonatomic, strong) UIView *selectedViewDescriptionContainer;
@property (nonatomic, strong) UIView *selectedViewColorIndicator;
@property (nonatomic, strong) UILabel *selectedViewDescriptionLabel;
@end
@implementation FLEXExplorerToolbar
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSMutableArray *toolbarItems = [NSMutableArray array];
self.dragHandle = [[UIView alloc] init];
self.dragHandle.backgroundColor = [FLEXToolbarItem defaultBackgroundColor];
[self addSubview:self.dragHandle];
UIImage *dragHandle = [FLEXResources dragHandle];
self.dragHandleImageView = [[UIImageView alloc] initWithImage:dragHandle];
[self.dragHandle addSubview:self.dragHandleImageView];
UIImage *globalsIcon = [FLEXResources globeIcon];
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"menu" image:globalsIcon];
[self addSubview:self.globalsItem];
[toolbarItems addObject:self.globalsItem];
UIImage *listIcon = [FLEXResources listIcon];
self.hierarchyItem = [FLEXToolbarItem toolbarItemWithTitle:@"views" image:listIcon];
[self addSubview:self.hierarchyItem];
[toolbarItems addObject:self.hierarchyItem];
UIImage *selectIcon = [FLEXResources selectIcon];
self.selectItem = [FLEXToolbarItem toolbarItemWithTitle:@"select" image:selectIcon];
[self addSubview:self.selectItem];
[toolbarItems addObject:self.selectItem];
UIImage *moveIcon = [FLEXResources moveIcon];
self.moveItem = [FLEXToolbarItem toolbarItemWithTitle:@"move" image:moveIcon];
[self addSubview:self.moveItem];
[toolbarItems addObject:self.moveItem];
UIImage *closeIcon = [FLEXResources closeIcon];
self.closeItem = [FLEXToolbarItem toolbarItemWithTitle:@"close" image:closeIcon];
[self addSubview:self.closeItem];
[toolbarItems addObject:self.closeItem];
self.toolbarItems = toolbarItems;
self.backgroundColor = [UIColor clearColor];
self.selectedViewDescriptionContainer = [[UIView alloc] init];
self.selectedViewDescriptionContainer.backgroundColor = [UIColor colorWithWhite:0.9 alpha:0.95];
self.selectedViewDescriptionContainer.hidden = YES;
[self addSubview:self.selectedViewDescriptionContainer];
self.selectedViewColorIndicator = [[UIView alloc] init];
self.selectedViewColorIndicator.backgroundColor = [UIColor redColor];
[self.selectedViewDescriptionContainer addSubview:self.selectedViewColorIndicator];
self.selectedViewDescriptionLabel = [[UILabel alloc] init];
self.selectedViewDescriptionLabel.backgroundColor = [UIColor clearColor];
self.selectedViewDescriptionLabel.font = [[self class] descriptionLabelFont];
[self.selectedViewDescriptionContainer addSubview:self.selectedViewDescriptionLabel];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
// Drag Handle
const CGFloat kToolbarItemHeight = [[self class] toolbarItemHeight];
self.dragHandle.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, [[self class] dragHandleWidth], kToolbarItemHeight);
CGRect dragHandleImageFrame = self.dragHandleImageView.frame;
dragHandleImageFrame.origin.x = FLEXFloor((self.dragHandle.frame.size.width - dragHandleImageFrame.size.width) / 2.0);
dragHandleImageFrame.origin.y = FLEXFloor((self.dragHandle.frame.size.height - dragHandleImageFrame.size.height) / 2.0);
self.dragHandleImageView.frame = dragHandleImageFrame;
// Toolbar Items
CGFloat originX = CGRectGetMaxX(self.dragHandle.frame);
CGFloat originY = self.bounds.origin.y;
CGFloat height = kToolbarItemHeight;
CGFloat width = FLEXFloor((CGRectGetMaxX(self.bounds) - originX) / [self.toolbarItems count]);
for (UIView *toolbarItem in self.toolbarItems) {
toolbarItem.frame = CGRectMake(originX, originY, width, height);
originX = CGRectGetMaxX(toolbarItem.frame);
}
// Make sure the last toolbar item goes to the edge to account for any accumulated rounding effects.
UIView *lastToolbarItem = [self.toolbarItems lastObject];
CGRect lastToolbarItemFrame = lastToolbarItem.frame;
lastToolbarItemFrame.size.width = CGRectGetMaxX(self.bounds) - lastToolbarItemFrame.origin.x;
lastToolbarItem.frame = lastToolbarItemFrame;
const CGFloat kSelectedViewColorDiameter = [[self class] selectedViewColorIndicatorDiameter];
const CGFloat kDescriptionLabelHeight = [[self class] descriptionLabelHeight];
const CGFloat kHorizontalPadding = [[self class] horizontalPadding];
const CGFloat kDescriptionVerticalPadding = [[self class] descriptionVerticalPadding];
const CGFloat kDescriptionContainerHeight = [[self class] descriptionContainerHeight];
CGRect descriptionContainerFrame = CGRectZero;
descriptionContainerFrame.size.height = kDescriptionContainerHeight;
descriptionContainerFrame.origin.y = CGRectGetMaxY(self.bounds) - kDescriptionContainerHeight;
descriptionContainerFrame.size.width = self.bounds.size.width;
self.selectedViewDescriptionContainer.frame = descriptionContainerFrame;
// Selected View Color
CGRect selectedViewColorFrame = CGRectZero;
selectedViewColorFrame.size.width = kSelectedViewColorDiameter;
selectedViewColorFrame.size.height = kSelectedViewColorDiameter;
selectedViewColorFrame.origin.x = kHorizontalPadding;
selectedViewColorFrame.origin.y = FLEXFloor((kDescriptionContainerHeight - kSelectedViewColorDiameter) / 2.0);
self.selectedViewColorIndicator.frame = selectedViewColorFrame;
self.selectedViewColorIndicator.layer.cornerRadius = ceil(selectedViewColorFrame.size.height / 2.0);
// Selected View Description
CGRect descriptionLabelFrame = CGRectZero;
CGFloat descriptionOriginX = CGRectGetMaxX(selectedViewColorFrame) + kHorizontalPadding;
descriptionLabelFrame.size.height = kDescriptionLabelHeight;
descriptionLabelFrame.origin.x = descriptionOriginX;
descriptionLabelFrame.origin.y = kDescriptionVerticalPadding;
descriptionLabelFrame.size.width = CGRectGetMaxX(self.selectedViewDescriptionContainer.bounds) - kHorizontalPadding - descriptionOriginX;
self.selectedViewDescriptionLabel.frame = descriptionLabelFrame;
}
#pragma mark - Setter Overrides
- (void)setSelectedViewOverlayColor:(UIColor *)selectedViewOverlayColor
{
if (![_selectedViewOverlayColor isEqual:selectedViewOverlayColor]) {
_selectedViewOverlayColor = selectedViewOverlayColor;
self.selectedViewColorIndicator.backgroundColor = selectedViewOverlayColor;
}
}
- (void)setSelectedViewDescription:(NSString *)selectedViewDescription
{
if (![_selectedViewDescription isEqual:selectedViewDescription]) {
_selectedViewDescription = selectedViewDescription;
self.selectedViewDescriptionLabel.text = selectedViewDescription;
BOOL showDescription = [selectedViewDescription length] > 0;
self.selectedViewDescriptionContainer.hidden = !showDescription;
}
}
#pragma mark - Sizing Convenience Methods
+ (UIFont *)descriptionLabelFont
{
return [UIFont systemFontOfSize:12.0];
}
+ (CGFloat)toolbarItemHeight
{
return 44.0;
}
+ (CGFloat)dragHandleWidth
{
return 30.0;
}
+ (CGFloat)descriptionLabelHeight
{
return ceil([[self descriptionLabelFont] lineHeight]);
}
+ (CGFloat)descriptionVerticalPadding
{
return 2.0;
}
+ (CGFloat)descriptionContainerHeight
{
return [self descriptionVerticalPadding] * 2.0 + [self descriptionLabelHeight];
}
+ (CGFloat)selectedViewColorIndicatorDiameter
{
return ceil([self descriptionLabelHeight] / 2.0);
}
+ (CGFloat)horizontalPadding
{
return 11.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat height = 0.0;
height += [[self class] toolbarItemHeight];
height += [[self class] descriptionContainerHeight];
return CGSizeMake(size.width, height);
}
@end
@@ -1,37 +0,0 @@
//
// FLEXExplorerViewController.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol FLEXExplorerViewControllerDelegate;
@interface FLEXExplorerViewController : UIViewController
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
- (BOOL)wantsWindowToBecomeKey;
// Keyboard shortcut helpers
- (void)toggleSelectTool;
- (void)toggleMoveTool;
- (void)toggleViewsTool;
- (void)toggleMenuTool;
- (void)handleDownArrowKeyPressed;
- (void)handleUpArrowKeyPressed;
- (void)handleRightArrowKeyPressed;
- (void)handleLeftArrowKeyPressed;
@end
@protocol FLEXExplorerViewControllerDelegate <NSObject>
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
@end
@@ -1,916 +0,0 @@
//
// FLEXExplorerViewController.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXExplorerViewController.h"
#import "FLEXExplorerToolbar.h"
#import "FLEXToolbarItem.h"
#import "FLEXUtility.h"
#import "FLEXHierarchyTableViewController.h"
#import "FLEXGlobalsTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXNetworkHistoryTableViewController.h"
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
FLEXExplorerModeDefault,
FLEXExplorerModeSelect,
FLEXExplorerModeMove
};
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
/// Tracks the currently active tool/mode
@property (nonatomic, assign) FLEXExplorerMode currentMode;
/// Gesture recognizer for dragging a view in move mode
@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
/// Gesture recognizer for showing additional details on the selected view
@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
/// Only valid while a move pan gesture is in progress.
@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic, assign) CGRect toolbarFrameBeforeDragging;
/// Borders of all the visible views in the hierarchy at the selection point.
/// The keys are NSValues with the correponding view (nonretained).
@property (nonatomic, strong) NSDictionary *outlineViewsForVisibleViews;
/// The actual views at the selection point with the deepest view last.
@property (nonatomic, strong) NSArray *viewsAtTapPoint;
/// The view that we're currently highlighting with an overlay and displaying details for.
@property (nonatomic, strong) UIView *selectedView;
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic, strong) UIView *selectedViewOverlay;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture intput.
/// If we're just showing the toolbar, we want the main app's window to remain key so that we don't interfere with input, status bar, etc.
@property (nonatomic, strong) UIWindow *previousKeyWindow;
/// Similar to the previousKeyWindow property above, we need to track status bar styling if
/// the app doesn't use view controller based status bar management. When we present a modal,
/// we want to change the status bar style to UIStausBarStyleDefault. Before changing, we stash
/// the current style. On dismissal, we return the staus bar to the style that the app was using previously.
@property (nonatomic, assign) UIStatusBarStyle previousStatusBarStyle;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic, strong) NSMutableSet *observedViews;
@end
@implementation FLEXExplorerViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.observedViews = [NSMutableSet set];
}
return self;
}
-(void)dealloc
{
for (UIView *view in _observedViews) {
[self stopObservingView:view];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Toolbar
self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:self.view.bounds.size];
// Start the toolbar off below any bars that may be at the top of the view.
CGFloat toolbarOriginY = 100.0;
self.explorerToolbar.frame = CGRectMake(0.0, toolbarOriginY, toolbarSize.width, toolbarSize.height);
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
[self.view addSubview:self.explorerToolbar];
[self setupToolbarActions];
[self setupToolbarGestures];
// View selection
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelectionTap:)];
[self.view addGestureRecognizer:selectionTapGR];
// View moving
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
[self.view addGestureRecognizer:self.movePanGR];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateButtonStates];
}
#pragma mark - Rotation
- (UIViewController *)viewControllerForRotationAndOrientation
{
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
UIViewController *viewController = window.rootViewController;
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
viewController = [viewController performSelector:viewControllerSelector];
#pragma clang diagnostic pop
}
return viewController;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
UIInterfaceOrientationMask supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
if (viewControllerToAsk && viewControllerToAsk != self) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
// The UIViewController docs state that this method must not return zero.
// If we weren't able to get a valid value for the supported interface orientations, default to all supported.
if (supportedOrientations == 0) {
supportedOrientations = UIInterfaceOrientationMaskAll;
}
return supportedOrientations;
}
- (BOOL)shouldAutorotate
{
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
}
return shouldAutorotate;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
}
}
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}
#pragma mark - Setter Overrides
- (void)setSelectedView:(UIView *)selectedView
{
if (![_selectedView isEqual:selectedView]) {
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
[self stopObservingView:_selectedView];
}
_selectedView = selectedView;
[self beginObservingView:selectedView];
// Update the toolbar and selected overlay
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:selectedView includingFrame:YES];
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];;
if (selectedView) {
if (!self.selectedViewOverlay) {
self.selectedViewOverlay = [[UIView alloc] init];
[self.view addSubview:self.selectedViewOverlay];
self.selectedViewOverlay.layer.borderWidth = 1.0;
}
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
self.selectedViewOverlay.layer.borderColor = [outlineColor CGColor];
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
// Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
[self.view bringSubviewToFront:self.selectedViewOverlay];
[self.view bringSubviewToFront:self.explorerToolbar];
} else {
[self.selectedViewOverlay removeFromSuperview];
self.selectedViewOverlay = nil;
}
// Some of the button states depend on whether we have a selected view.
[self updateButtonStates];
}
}
- (void)setViewsAtTapPoint:(NSArray *)viewsAtTapPoint
{
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
for (UIView *view in _viewsAtTapPoint) {
if (view != self.selectedView) {
[self stopObservingView:view];
}
}
_viewsAtTapPoint = viewsAtTapPoint;
for (UIView *view in viewsAtTapPoint) {
[self beginObservingView:view];
}
}
}
- (void)setCurrentMode:(FLEXExplorerMode)currentMode
{
if (_currentMode != currentMode) {
_currentMode = currentMode;
switch (currentMode) {
case FLEXExplorerModeDefault:
[self removeAndClearOutlineViews];
self.viewsAtTapPoint = nil;
self.selectedView = nil;
break;
case FLEXExplorerModeSelect:
// Make sure the outline views are unhidden in case we came from the move mode.
for (id key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = NO;
}
break;
case FLEXExplorerModeMove:
// Hide all the outline views to focus on the selected view, which is the only one that will move.
for (id key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = YES;
}
break;
}
self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
[self updateButtonStates];
}
}
#pragma mark - View Tracking
- (void)beginObservingView:(UIView *)view
{
// Bail if we're already observing this view or if there's nothing to observe.
if (!view || [self.observedViews containsObject:view]) {
return;
}
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
}
[self.observedViews addObject:view];
}
- (void)stopObservingView:(UIView *)view
{
if (!view) {
return;
}
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
[view removeObserver:self forKeyPath:keyPath];
}
[self.observedViews removeObject:view];
}
+ (NSArray *)viewKeyPathsToTrack
{
static NSArray *trackedViewKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
trackedViewKeyPaths = @[frameKeyPath];
});
return trackedViewKeyPaths;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
}
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object
{
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
if (indexOfView != NSNotFound) {
UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
if (outline) {
outline.frame = [self frameInLocalCoordinatesForView:view];
}
}
if (object == self.selectedView) {
// Update the selected view description since we show the frame value there.
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:self.selectedView includingFrame:YES];
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
}
}
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view
{
// First convert to window coordinates since the view may be in a different window than our view.
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
// Then convert from the window to our view's coordinate space.
return [self.view convertRect:frameInWindow fromView:nil];
}
#pragma mark - Toolbar Buttons
- (void)setupToolbarActions
{
[self.explorerToolbar.selectItem addTarget:self action:@selector(selectButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.hierarchyItem addTarget:self action:@selector(hierarchyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.moveItem addTarget:self action:@selector(moveButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.globalsItem addTarget:self action:@selector(globalsButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.closeItem addTarget:self action:@selector(closeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)selectButtonTapped:(FLEXToolbarItem *)sender
{
[self toggleSelectTool];
}
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
{
[self toggleViewsTool];
}
- (NSArray *)allViewsInHierarchy
{
NSMutableArray *allViews = [NSMutableArray array];
NSArray *windows = [self allWindows];
for (UIWindow *window in windows) {
if (window != self.view.window) {
[allViews addObject:window];
[allViews addObjectsFromArray:[self allRecursiveSubviewsInView:window]];
}
}
return allViews;
}
- (NSArray *)allWindows
{
BOOL includeInternalWindows = YES;
BOOL onlyVisibleWindows = NO;
NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = [UIWindow class];
invocation.selector = allWindowsSelector;
[invocation setArgument:&includeInternalWindows atIndex:2];
[invocation setArgument:&onlyVisibleWindows atIndex:3];
[invocation invoke];
__unsafe_unretained NSArray *windows = nil;
[invocation getReturnValue:&windows];
return windows;
}
- (UIWindow *)statusWindow
{
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [[UIApplication sharedApplication] valueForKey:statusBarString];
}
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
{
[self toggleMoveTool];
}
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
{
[self toggleMenuTool];
}
- (void)closeButtonTapped:(FLEXToolbarItem *)sender
{
self.currentMode = FLEXExplorerModeDefault;
[self.delegate explorerViewControllerDidFinish:self];
}
- (void)updateButtonStates
{
// Move and details only active when an object is selected.
BOOL hasSelectedObject = self.selectedView != nil;
self.explorerToolbar.moveItem.enabled = hasSelectedObject;
self.explorerToolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
self.explorerToolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
}
#pragma mark - Toolbar Dragging
- (void)setupToolbarGestures
{
// Pan gesture for dragging.
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarPanGesture:)];
[self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
// Tap gesture for hinting.
UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarHintTapGesture:)];
[self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
// Tap gesture for showing additional details
self.detailsTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)];
[self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
}
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR
{
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
[self updateToolbarPostionWithDragGesture:panGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateToolbarPostionWithDragGesture:panGR];
break;
default:
break;
}
}
- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
{
CGPoint translation = [panGR translationInView:self.view];
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
newToolbarFrame.origin.y += translation.y;
CGFloat maxY = CGRectGetMaxY(self.view.bounds) - newToolbarFrame.size.height;
if (newToolbarFrame.origin.y < 0.0) {
newToolbarFrame.origin.y = 0.0;
} else if (newToolbarFrame.origin.y > maxY) {
newToolbarFrame.origin.y = maxY;
}
self.explorerToolbar.frame = newToolbarFrame;
}
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR
{
// Bounce the toolbar to indicate that it is draggable.
// TODO: make it bouncier.
if (tapGR.state == UIGestureRecognizerStateRecognized) {
CGRect originalToolbarFrame = self.explorerToolbar.frame;
const NSTimeInterval kHalfwayDuration = 0.2;
const CGFloat kVerticalOffset = 30.0;
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
CGRect newToolbarFrame = self.explorerToolbar.frame;
newToolbarFrame.origin.y += kVerticalOffset;
self.explorerToolbar.frame = newToolbarFrame;
} completion:^(BOOL finished) {
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.explorerToolbar.frame = originalToolbarFrame;
} completion:nil];
}];
}
}
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR
{
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
FLEXObjectExplorerViewController *selectedViewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
selectedViewExplorer.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(selectedViewExplorerFinished:)];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:selectedViewExplorer];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
}
}
#pragma mark - View Selection
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR
{
// Only if we're in selection mode
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
// Note that [tapGR locationInView:nil] is broken in iOS 8, so we have to do a two step conversion to window coordinates.
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
CGPoint tapPointInView = [tapGR locationInView:self.view];
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
}
}
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow
{
[self removeAndClearOutlineViews];
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
// For outlined views and the selected view, only use visible views.
// Outlining hidden views adds clutter and makes the selection behavior confusing.
NSArray *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
NSMutableDictionary *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
for (UIView *view in visibleViewsAtTapPoint) {
UIView *outlineView = [self outlineViewForView:view];
[self.view addSubview:outlineView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
[newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
}
self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
// Make sure the explorer toolbar doesn't end up behind the newly added outline views.
[self.view bringSubviewToFront:self.explorerToolbar];
[self updateButtonStates];
}
- (UIView *)outlineViewForView:(UIView *)view
{
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
outlineView.backgroundColor = [UIColor clearColor];
outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
outlineView.layer.borderWidth = 1.0;
return outlineView;
}
- (void)removeAndClearOutlineViews
{
for (id key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
[outlineView removeFromSuperview];
}
self.outlineViewsForVisibleViews = nil;
}
- (NSArray *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
{
NSMutableArray *views = [NSMutableArray array];
for (UIWindow *window in [self allWindows]) {
// Don't include the explorer's own window or subviews.
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
[views addObject:window];
[views addObjectsFromArray:[self recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden]];
}
}
return views;
}
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow
{
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
// Default to the the application's key window if none of the windows want the touch.
UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
for (UIWindow *window in [[self allWindows] reverseObjectEnumerator]) {
// Ignore the explorer's own window.
if (window != self.view.window) {
if ([window hitTest:tapPointInWindow withEvent:nil]) {
windowForSelection = window;
break;
}
}
}
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
return [[self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES] lastObject];
}
- (NSArray *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
{
NSMutableArray *subviewsAtPoint = [NSMutableArray array];
for (UIView *subview in view.subviews) {
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
if (skipHidden && isHidden) {
continue;
}
BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
if (subviewContainsPoint) {
[subviewsAtPoint addObject:subview];
}
// If this view doesn't clip to its bounds, we need to check its subviews even if it doesn't contain the selection point.
// They may be visible and contain the selection point.
if (subviewContainsPoint || !subview.clipsToBounds) {
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
[subviewsAtPoint addObjectsFromArray:[self recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden]];
}
}
return subviewsAtPoint;
}
- (NSArray *)allRecursiveSubviewsInView:(UIView *)view
{
NSMutableArray *subviews = [NSMutableArray array];
for (UIView *subview in view.subviews) {
[subviews addObject:subview];
[subviews addObjectsFromArray:[self allRecursiveSubviewsInView:subview]];
}
return subviews;
}
- (NSDictionary *)hierarchyDepthsForViews:(NSArray *)views
{
NSMutableDictionary *hierarchyDepths = [NSMutableDictionary dictionary];
for (UIView *view in views) {
NSInteger depth = 0;
UIView *tryView = view;
while (tryView.superview) {
tryView = tryView.superview;
depth++;
}
[hierarchyDepths setObject:@(depth) forKey:[NSValue valueWithNonretainedObject:view]];
}
return hierarchyDepths;
}
#pragma mark - Selected View Moving
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR
{
switch (movePanGR.state) {
case UIGestureRecognizerStateBegan:
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
[self updateSelectedViewPositionWithDragGesture:movePanGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateSelectedViewPositionWithDragGesture:movePanGR];
break;
default:
break;
}
}
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR
{
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
self.selectedView.frame = newSelectedViewFrame;
}
#pragma mark - Touch Handling
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates
{
BOOL shouldReceiveTouch = NO;
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
// Always if it's on the toolbar
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
shouldReceiveTouch = YES;
}
// Always if we're in selection mode
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
shouldReceiveTouch = YES;
}
// Always in move mode too
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
shouldReceiveTouch = YES;
}
// Always if we have a modal presented
if (!shouldReceiveTouch && self.presentedViewController) {
shouldReceiveTouch = YES;
}
return shouldReceiveTouch;
}
#pragma mark - FLEXHierarchyTableViewControllerDelegate
- (void)hierarchyViewController:(FLEXHierarchyTableViewController *)hierarchyViewController didFinishWithSelectedView:(UIView *)selectedView
{
// Note that we need to wait until the view controller is dismissed to calculated the frame of the outline view.
// Otherwise the coordinate conversion doesn't give the correct result.
[self resignKeyAndDismissViewControllerAnimated:YES completion:^{
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
// then clear out the tap point array and remove all the outline views.
if (![self.viewsAtTapPoint containsObject:selectedView]) {
self.viewsAtTapPoint = nil;
[self removeAndClearOutlineViews];
}
// If we now have a selected view and we didn't have one previously, go to "select" mode.
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
self.currentMode = FLEXExplorerModeSelect;
}
// The selected view setter will also update the selected view overlay appropriately.
self.selectedView = selectedView;
}];
}
#pragma mark - FLEXGlobalsViewControllerDelegate
- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController
{
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - FLEXObjectExplorerViewController Done Action
- (void)selectedViewExplorerFinished:(id)sender
{
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - Modal Presentation and Window Management
- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
{
// Save the current key window so we can restore it following dismissal.
self.previousKeyWindow = [[UIApplication sharedApplication] keyWindow];
// Make our window key to correctly handle input.
[self.view.window makeKeyWindow];
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
// for view controller based management because the global methods no-op if that is turned on.
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
}
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
{
UIWindow *previousKeyWindow = self.previousKeyWindow;
self.previousKeyWindow = nil;
[previousKeyWindow makeKeyWindow];
[[previousKeyWindow rootViewController] setNeedsStatusBarAppearanceUpdate];
// Restore the status bar window's normal window level.
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
// Restore the stauts bar style if the app is using global status bar management.
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
[self dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
return self.previousKeyWindow != nil;
}
#pragma mark - Keyboard Shortcut Helpers
- (void)toggleSelectTool
{
if (self.currentMode == FLEXExplorerModeSelect) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeSelect;
}
}
- (void)toggleMoveTool
{
if (self.currentMode == FLEXExplorerModeMove) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeMove;
}
}
- (void)toggleViewsTool
{
BOOL viewsModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
viewsModalShown = viewsModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXHierarchyTableViewController class]];
if (viewsModalShown) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
} else {
[self resignKeyAndDismissViewControllerAnimated:NO completion:nil];
NSArray *allViews = [self allViewsInHierarchy];
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
hierarchyTVC.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
}
}
- (void)toggleMenuTool
{
BOOL menuModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
menuModalShown = menuModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXGlobalsTableViewController class]];
if (menuModalShown) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
} else {
[self resignKeyAndDismissViewControllerAnimated:NO completion:nil];
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
globalsViewController.delegate = self;
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
}
}
- (void)handleDownArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
}
}
}
- (void)handleUpArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex < [self.viewsAtTapPoint count] - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
}
}
- (void)handleRightArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
}
}
- (void)handleLeftArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
}
}
@end
@@ -1,16 +0,0 @@
//
// FLEXManager+Private.h
// PebbleApp
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 Pebble Technology. All rights reserved.
//
#import "FLEXManager.h"
@interface FLEXManager ()
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
@property (nonatomic, readonly, strong) NSArray *userGlobalEntries;
@end
-70
View File
@@ -1,70 +0,0 @@
//
// FLEXManager.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, readonly) BOOL isHidden;
- (void)showExplorer;
- (void)hideExplorer;
- (void)toggleExplorer;
#pragma mark - Network Debugging
/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
/// Full responses are kept temporarily in a size limited cache and may be pruged under memory pressure.
@property (nonatomic, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
#pragma mark - Keyboard Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
/// @param modifiers Modifier keys such as shift, command, or alt/option
/// @param action The block to run on the main thread when the key & modifier combination is recognized.
/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
/// @note The action block will be retained for the duration of the application. You may want to use weak references.
/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
#pragma mark - Extensions
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
/// @note This method must be called from the main thread.
/// The objectFutureBlock will be invoked from the main thread and may return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
@end
-360
View File
@@ -1,360 +0,0 @@
//
// FLEXManager.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXManager.h"
#import "FLEXExplorerViewController.h"
#import "FLEXWindow.h"
#import "FLEXGlobalsTableViewControllerEntry.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXKeyboardShortcutManager.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXKeyboardHelpViewController.h"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
@property (nonatomic, strong) FLEXWindow *explorerWindow;
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
@end
@implementation FLEXManager
+ (instancetype)sharedManager
{
static FLEXManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[[self class] alloc] init];
});
return sharedManager;
}
- (instancetype)init
{
self = [super init];
if (self) {
_userGlobalEntries = [[NSMutableArray alloc] init];
}
return self;
}
- (FLEXWindow *)explorerWindow
{
NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
if (!_explorerWindow) {
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
_explorerWindow.eventDelegate = self;
_explorerWindow.rootViewController = self.explorerViewController;
}
return _explorerWindow;
}
- (FLEXExplorerViewController *)explorerViewController
{
if (!_explorerViewController) {
_explorerViewController = [[FLEXExplorerViewController alloc] init];
_explorerViewController.delegate = self;
}
return _explorerViewController;
}
- (void)showExplorer
{
self.explorerWindow.hidden = NO;
}
- (void)hideExplorer
{
self.explorerWindow.hidden = YES;
}
- (void)toggleExplorer {
if (self.explorerWindow.isHidden) {
[self showExplorer];
} else {
[self hideExplorer];
}
}
- (BOOL)isHidden
{
return self.explorerWindow.isHidden;
}
- (BOOL)isNetworkDebuggingEnabled
{
return [FLEXNetworkObserver isEnabled];
}
- (void)setNetworkDebuggingEnabled:(BOOL)networkDebuggingEnabled
{
[FLEXNetworkObserver setEnabled:networkDebuggingEnabled];
}
- (NSUInteger)networkResponseCacheByteLimit
{
return [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
}
- (void)setNetworkResponseCacheByteLimit:(NSUInteger)networkResponseCacheByteLimit
{
[[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:networkResponseCacheByteLimit];
}
#pragma mark - FLEXWindowEventDelegate
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
{
// Ask the explorer view controller
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
}
- (BOOL)canBecomeKeyWindow
{
// Only when the explorer view controller wants it because it needs to accept key input & affect the status bar.
return [self.explorerViewController wantsWindowToBecomeKey];
}
#pragma mark - FLEXExplorerViewControllerDelegate
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
{
[self hideExplorer];
}
#pragma mark - Simulator Shortcuts
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
{
# if TARGET_OS_SIMULATOR
[[FLEXKeyboardShortcutManager sharedManager] registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description];
#endif
}
- (void)setSimulatorShortcutsEnabled:(BOOL)simulatorShortcutsEnabled
{
# if TARGET_OS_SIMULATOR
[[FLEXKeyboardShortcutManager sharedManager] setEnabled:simulatorShortcutsEnabled];
#endif
}
- (BOOL)simulatorShortcutsEnabled
{
# if TARGET_OS_SIMULATOR
return [[FLEXKeyboardShortcutManager sharedManager] isEnabled];
#else
return NO;
#endif
}
- (void)registerDefaultSimulatorShortcuts
{
[self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
[self toggleExplorer];
} description:@"Toggle FLEX toolbar"];
[self registerSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMenuTool];
} description:@"Toggle FLEX globlas menu"];
[self registerSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleViewsTool];
} description:@"Toggle view hierarchy menu"];
[self registerSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleSelectTool];
} description:@"Toggle select tool"];
[self registerSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMoveTool];
} description:@"Toggle move tool"];
[self registerSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXNetworkHistoryTableViewController class]];
} description:@"Toggle network history view"];
[self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
if ([self isHidden]) {
[self tryScrollDown];
} else {
[self.explorerViewController handleDownArrowKeyPressed];
}
} description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
[self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
if ([self isHidden]) {
[self tryScrollUp];
} else {
[self.explorerViewController handleUpArrowKeyPressed];
}
} description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
[self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
if (![self isHidden]) {
[self.explorerViewController handleRightArrowKeyPressed];
}
} description:@"Move selected view right"];
[self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
if ([self isHidden]) {
[self tryGoBack];
} else {
[self.explorerViewController handleLeftArrowKeyPressed];
}
} description:@"Move selected view left"];
[self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
} description:@"Toggle (this) help menu"];
[self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
[[[self topViewController] presentingViewController] dismissViewControllerAnimated:YES completion:nil];
} description:@"End editing text\n\t\tDismiss top view controller"];
[self registerSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
[self toggleTopViewControllerOfClass:[FLEXFileBrowserTableViewController class]];
} description:@"Toggle file browser menu"];
}
+ (void)load
{
dispatch_async(dispatch_get_main_queue(), ^{
[[[self class] sharedManager] registerDefaultSimulatorShortcuts];
});
}
#pragma mark - Extensions
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
{
NSParameterAssert(entryName);
NSParameterAssert(objectFutureBlock);
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
entryName = entryName.copy;
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
return entryName;
} viewControllerFuture:^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
}];
[self.userGlobalEntries addObject:entry];
}
- (void)registerGlobalEntryWithName:(NSString *)entryName viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock
{
NSParameterAssert(entryName);
NSParameterAssert(viewControllerFutureBlock);
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
entryName = entryName.copy;
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
return entryName;
} viewControllerFuture:^UIViewController *{
UIViewController *viewController = viewControllerFutureBlock();
NSCAssert(viewController, @"'%@' entry returned nil viewController. viewControllerFutureBlock should never return nil.", entryName);
return viewController;
}];
[self.userGlobalEntries addObject:entry];
}
- (void)tryScrollDown
{
UIScrollView *firstScrollView = [self firstScrollView];
CGPoint contentOffset = [firstScrollView contentOffset];
CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
CGFloat maxContentOffsetY = firstScrollView.contentSize.height + firstScrollView.contentInset.bottom - firstScrollView.frame.size.height;
distance = MIN(maxContentOffsetY - firstScrollView.contentOffset.y, distance);
contentOffset.y += distance;
[firstScrollView setContentOffset:contentOffset animated:YES];
}
- (void)tryScrollUp
{
UIScrollView *firstScrollView = [self firstScrollView];
CGPoint contentOffset = [firstScrollView contentOffset];
CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
CGFloat minContentOffsetY = -firstScrollView.contentInset.top;
distance = MIN(firstScrollView.contentOffset.y - minContentOffsetY, distance);
contentOffset.y -= distance;
[firstScrollView setContentOffset:contentOffset animated:YES];
}
- (UIScrollView *)firstScrollView
{
NSMutableArray *views = [[[[UIApplication sharedApplication] keyWindow] subviews] mutableCopy];
UIScrollView *scrollView = nil;
while ([views count] > 0) {
UIView *view = [views firstObject];
[views removeObjectAtIndex:0];
if ([view isKindOfClass:[UIScrollView class]]) {
scrollView = (UIScrollView *)view;
break;
} else {
[views addObjectsFromArray:[view subviews]];
}
}
return scrollView;
}
- (void)tryGoBack
{
UINavigationController *navigationController = nil;
UIViewController *topViewController = [self topViewController];
if ([topViewController isKindOfClass:[UINavigationController class]]) {
navigationController = (UINavigationController *)topViewController;
} else {
navigationController = topViewController.navigationController;
}
[navigationController popViewControllerAnimated:YES];
}
- (UIViewController *)topViewController
{
UIViewController *topViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
while ([topViewController presentedViewController]) {
topViewController = [topViewController presentedViewController];
}
return topViewController;
}
- (void)toggleTopViewControllerOfClass:(Class)class
{
UIViewController *topViewController = [self topViewController];
if ([topViewController isKindOfClass:[UINavigationController class]] && [[[(UINavigationController *)topViewController viewControllers] firstObject] isKindOfClass:[class class]]) {
[[topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
} else {
id viewController = [[class alloc] init];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[topViewController presentViewController:navigationController animated:YES completion:nil];
}
}
- (void)showExplorerIfNeeded
{
if ([self isHidden]) {
[self showExplorer];
}
}
@end
-17
View File
@@ -1,17 +0,0 @@
//
// FLEXToolbarItem.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXToolbarItem : UIButton
+ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image;
+ (UIColor *)defaultBackgroundColor;
@end
-133
View File
@@ -1,133 +0,0 @@
//
// FLEXToolbarItem.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXToolbarItem.h"
#import "FLEXUtility.h"
@interface FLEXToolbarItem ()
@property (nonatomic, copy) NSAttributedString *attributedTitle;
@property (nonatomic, strong) UIImage *image;
@end
@implementation FLEXToolbarItem
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [[self class] defaultBackgroundColor];
[self setTitleColor:[[self class] defaultTitleColor] forState:UIControlStateNormal];
[self setTitleColor:[[self class] disabledTitleColor] forState:UIControlStateDisabled];
}
return self;
}
+ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image
{
FLEXToolbarItem *toolbarItem = [self buttonWithType:UIButtonTypeCustom];
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:[self titleAttributes]];
toolbarItem.attributedTitle = attributedTitle;
toolbarItem.image = image;
[toolbarItem setAttributedTitle:attributedTitle forState:UIControlStateNormal];
[toolbarItem setImage:image forState:UIControlStateNormal];
return toolbarItem;
}
#pragma mark - Display Defaults
+ (NSDictionary *)titleAttributes
{
return @{NSFontAttributeName : [FLEXUtility defaultFontOfSize:12.0]};
}
+ (UIColor *)defaultTitleColor
{
return [UIColor blackColor];
}
+ (UIColor *)disabledTitleColor
{
return [UIColor colorWithWhite:121.0/255.0 alpha:1.0];
}
+ (UIColor *)highlightedBackgroundColor
{
return [UIColor colorWithWhite:0.9 alpha:1.0];
}
+ (UIColor *)selectedBackgroundColor
{
return [UIColor colorWithRed:199.0/255.0 green:199.0/255.0 blue:255.0/255.0 alpha:1.0];
}
+ (UIColor *)defaultBackgroundColor
{
return [UIColor colorWithWhite:1.0 alpha:0.95];
}
+ (CGFloat)topMargin
{
return 2.0;
}
#pragma mark - State Changes
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[self updateBackgroundColor];
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
[self updateBackgroundColor];
}
- (void)updateBackgroundColor
{
if (self.highlighted) {
self.backgroundColor = [[self class] highlightedBackgroundColor];
} else if (self.selected) {
self.backgroundColor = [[self class] selectedBackgroundColor];
} else {
self.backgroundColor = [[self class] defaultBackgroundColor];
}
}
#pragma mark - UIButton Layout Overrides
- (CGRect)titleRectForContentRect:(CGRect)contentRect
{
// Bottom aligned and centered.
CGRect titleRect = CGRectZero;
CGSize titleSize = [self.attributedTitle boundingRectWithSize:contentRect.size options:0 context:nil].size;
titleSize = CGSizeMake(ceil(titleSize.width), ceil(titleSize.height));
titleRect.size = titleSize;
titleRect.origin.y = contentRect.origin.y + CGRectGetMaxY(contentRect) - titleSize.height;
titleRect.origin.x = contentRect.origin.x + FLEXFloor((contentRect.size.width - titleSize.width) / 2.0);
return titleRect;
}
- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
CGSize imageSize = self.image.size;
CGRect titleRect = [self titleRectForContentRect:contentRect];
CGFloat availableHeight = contentRect.size.height - titleRect.size.height - [[self class] topMargin];
CGFloat originY = [[self class] topMargin] + FLEXFloor((availableHeight - imageSize.height) / 2.0);
CGFloat originX = FLEXFloor((contentRect.size.width - imageSize.width) / 2.0);
CGRect imageRect = CGRectMake(originX, originY, imageSize.width, imageSize.height);
return imageRect;
}
@end
-24
View File
@@ -1,24 +0,0 @@
//
// FLEXWindow.h
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol FLEXWindowEventDelegate;
@interface FLEXWindow : UIWindow
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
@end
@protocol FLEXWindowEventDelegate <NSObject>
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
- (BOOL)canBecomeKeyWindow;
@end
+27
View File
@@ -0,0 +1,27 @@
//
// FLEX-Categories.h
// FLEX
//
// Created by Tanner on 3/12/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#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>
+23
View File
@@ -0,0 +1,23 @@
//
// FLEX-Core.h
// FLEX
//
// Created by Tanner on 3/11/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <FLEX/FLEXFilteringTableViewController.h>
#import <FLEX/FLEXNavigationController.h>
#import <FLEX/FLEXTableViewController.h>
#import <FLEX/FLEXTableView.h>
#import <FLEX/FLEXSingleRowSection.h>
#import <FLEX/FLEXTableViewSection.h>
#import <FLEX/FLEXCodeFontCell.h>
#import <FLEX/FLEXSubtitleTableViewCell.h>
#import <FLEX/FLEXTableViewCell.h>
#import <FLEX/FLEXMultilineTableViewCell.h>
#import <FLEX/FLEXKeyValueTableViewCell.h>
#import <FLEX/FLEXScopeCarousel.h>
+30
View File
@@ -0,0 +1,30 @@
//
// FLEX-ObjectExploring.h
// FLEX
//
// Created by Tanner on 3/11/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <FLEX/FLEXObjectExplorerFactory.h>
#import <FLEX/FLEXObjectExplorerViewController.h>
#import <FLEX/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 <FLEX/FLEXCollectionContentSection.h>
#import <FLEX/FLEXColorPreviewSection.h>
#import <FLEX/FLEXDefaultsContentSection.h>
#import <FLEX/FLEXMetadataSection.h>
#import <FLEX/FLEXMutableListSection.h>
#import <FLEX/FLEXObjectInfoSection.h>

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