Compare commits

...

217 Commits

Author SHA1 Message Date
Tanner Bennett c761865b9b Namespace all fishhook functions, fix #408
I know not all of them needed to be namespaced as the private functions are static, but I did it anyway for consistency.
2020-04-06 17:47:47 -05:00
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
659 changed files with 37754 additions and 20698 deletions
+3
View File
@@ -17,3 +17,6 @@ DerivedData
*.ipa
*.xcuserstate
.DS_Store
/Example/Pods
Podfile.lock
IDEWorkspaceChecks.plist
@@ -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
@@ -7,7 +7,8 @@
//
#import <UIKit/UIKit.h>
@class FLEXScopeCarousel;
#import "FLEXTableView.h"
@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
typedef CGFloat FLEXDebounceInterval;
/// No delay, all events delivered
@@ -19,13 +20,35 @@ extern CGFloat const kFLEXDebounceForAsyncSearch;
/// The least frequent, at just over once per second; for I/O or other expensive operations
extern CGFloat const kFLEXDebounceForExpensiveIO;
@interface FLEXTableViewController : UITableViewController <UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate>
@protocol FLEXSearchResultsUpdating <NSObject>
/// 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 initWithStyle:
///
/// 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.
@@ -36,7 +59,7 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) FLEXScopeCarousel *carousel;
/// Defaults to NO.
///
///
/// Setting this to YES will initialize searchController and the view.
@property (nonatomic) BOOL showsSearchBar;
/// Defaults to NO.
@@ -46,22 +69,21 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) BOOL showSearchBarInitially;
/// nil unless showsSearchBar is set to YES.
///
///
/// self is used as the default search results updater and delegate.
/// Make sure your subclass conforms to UISearchControllerDelegate.
/// The search bar will not dim the background or hide the navigation bar by default.
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
@property (nonatomic) UISearchController *searchController;
/// Used to initialize the search controller. Defaults to nil.
@property (nonatomic) UIViewController *searchResultsController;
/// Defaults to "Fast"
///
///
/// Determines how often search bar results will be "debounced."
/// Empty query events are always sent instantly. Query events will
/// be sent when the user has not changed the query for this interval.
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
/// Whether the search bar stays at the top of the view while scrolling.
///
///
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
/// or it will not be respsected. Use this instead.
@@ -69,7 +91,7 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) BOOL pinSearchBar;
/// By default, we will show the search bar's cancel button when
/// search becomes active and hide it when search is dismissed.
///
///
/// Do not set the showsCancelButton property on the searchController's
/// searchBar manually. Set this property after turning on showsSearchBar.
///
@@ -78,19 +100,50 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
@property (nonatomic, readonly) NSInteger selectedScope;
@property (nonatomic) NSInteger selectedScope;
/// self.searchController.searchBar.text
@property (nonatomic, readonly) NSString *searchText;
/// Subclasses should override to handle search query update events.
///
/// searchBarDebounceInterval is used to reduce the frequency at which this method is called.
/// This method is also called when the search bar becomes the first responder,
/// and when the selected search bar scope index changes.
- (void)updateSearchResults:(NSString *)newText;
/// 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
-293
View File
@@ -1,293 +0,0 @@
//
// FLEXTableViewController.m
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@interface Block : NSObject
- (void)invoke;
@end
CGFloat const kFLEXDebounceInstant = 0.f;
CGFloat const kFLEXDebounceFast = 0.05;
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
@interface FLEXTableViewController ()
@property (nonatomic) NSTimer *debounceTimer;
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
@end
@implementation FLEXTableViewController
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
#pragma mark - Public
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
}
return self;
}
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
if (_showsSearchBar == showsSearchBar) return;
_showsSearchBar = showsSearchBar;
UIViewController *results = self.searchResultsController;
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
self.searchController.searchBar.placeholder = @"Filter";
self.searchController.searchResultsUpdater = (id)self;
self.searchController.delegate = (id)self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.hidesNavigationBarDuringPresentation = NO;
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
self.searchController.searchBar.delegate = self;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = self.searchController;
} else {
self.tableView.tableHeaderView = self.searchController.searchBar;
}
}
- (void)setShowsCarousel:(BOOL)showsCarousel {
if (_showsCarousel == showsCarousel) return;
_showsCarousel = showsCarousel;
_carousel = ({
__weak __typeof(self) weakSelf = self;
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
[self updateSearchResults:self.searchText];
};
self.tableView.tableHeaderView = carousel;
[self.tableView layoutIfNeeded];
// UITableView won't update the header size unless you reset the header view
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
__typeof(self) self = weakSelf;
self.tableView.tableHeaderView = carousel;
[self.tableView layoutIfNeeded];
}];
carousel;
});
}
- (NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
return self.searchController.searchBar.selectedScopeButtonIndex;
} else if (self.showsCarousel) {
return self.carousel.selectedIndex;
} else {
return NSNotFound;
}
}
- (NSString *)searchText {
return self.searchController.searchBar.text;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
- (void)updateSearchResults:(NSString *)newText { }
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *items = backgroundBlock();
dispatch_async(dispatch_get_main_queue(), ^{
mainBlock(items);
});
});
}
#pragma mark - View Controller Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// On iOS 13, the root view controller shows it's search bar no matter what.
// Turning this off avoids some weird flash the navigation bar does when we
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
// will still happen on subsequent view controllers, but we can at least
// avoid it for the root view controller
if (@available(iOS 13, *)) {
if (self.navigationController.viewControllers.firstObject == self) {
_showSearchBarInitially = NO;
}
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Allow scrolling to collapse the search bar, only if we don't want it pinned
if (@available(iOS 11.0, *)) {
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
// the search bar appear initially results in a bugged search bar that
// becomes transparent and floats over the screen as you scroll
[UIView animateWithDuration:0.2 animations:^{
self.navigationItem.hidesSearchBarWhenScrolling = YES;
[self.navigationController.view setNeedsLayout];
[self.navigationController.view layoutIfNeeded];
}];
}
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
// Reset this since we are re-appearing under a new
// parent view controller and need to show it again
self.didInitiallyRevealSearchBar = NO;
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if (self.searchController.active) {
self.searchController.active = NO;
}
}
#pragma mark - Private
- (void)debounce:(void(^)(void))block {
[self.debounceTimer invalidate];
self.debounceTimer = [NSTimer
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
target:block
selector:@selector(invoke)
userInfo:nil
repeats:NO
];
}
#pragma mark - Search Bar
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
[self.debounceTimer invalidate];
NSString *text = searchController.searchBar.text;
// Only debounce if we want to, and if we have a non-empty string
// Empty string events are sent instantly
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
[self debounce:^{
[self updateSearchResults:text];
}];
} else {
[self updateSearchResults:text];
}
}
#pragma mark UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController *)searchController {
// Manually show cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:YES animated:YES];
}
}
- (void)willDismissSearchController:(UISearchController *)searchController {
// Manually hide cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:NO animated:YES];
}
}
#pragma mark UISearchBarDelegate
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
[self updateSearchResultsForSearchController:self.searchController];
}
#pragma mark Table view
/// Not having a title in the first section looks weird with a rounded-corner table view style
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (@available(iOS 13, *)) {
return @" "; // For inset grouped style
}
return nil; // For plain/gropued style
}
@end
+115 -20
View File
@@ -2,38 +2,133 @@
// FLEXTableViewSection.h
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
NS_ASSUME_NONNULL_BEGIN
/// A protocol for arbitrary case-insensitive pattern matching
@protocol FLEXPatternMatching <NSObject>
/// @return YES if the receiver matches the query, case-insensitive
- (BOOL)matches:(NSString *)query;
@end
#pragma mark FLEXTableViewSection
@interface FLEXTableViewSection<__covariant ObjectType> : NSObject
/// An abstract base class for table view sections.
///
/// Many properties or methods here return nil or some logical equivalent by default.
/// Even so, most of the methods with defaults are intended to be overriden by subclasses.
/// Some methods are not implemented at all and MUST be implemented by a subclass.
@interface FLEXTableViewSection : NSObject {
@protected
/// Unused by default, use if you want
NSString *_title;
}
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray<ObjectType<FLEXPatternMatching>> *)rows;
#pragma mark - Data
@property (nonatomic, readonly) NSInteger section;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSArray<ObjectType<FLEXPatternMatching>> *rows;
/// A title to be displayed for the custom section.
/// Subclasses may override or use the \c _title ivar.
@property (nonatomic, readonly, nullable) NSString *title;
/// The number of rows in this section. Subclasses must override.
/// This should not change until \c filterText is changed or \c reloadData is called.
@property (nonatomic, readonly) NSInteger numberOfRows;
/// A map of reuse identifiers to \c UITableViewCell (sub)class objects.
/// Subclasses \e may override this as necessary, but are not required to.
/// See \c FLEXTableView.h for more information.
/// @return nil by default.
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping;
@property (nonatomic, readonly) NSInteger count;
/// The section should filter itself based on the contents of this property
/// as it is set. If it is set to nil or an empty string, it should not filter.
/// Subclasses should override or observe this property and react to changes.
///
/// It is common practice to use two arrays for the underlying model:
/// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText:
/// is called, call \c super to store the new value, and re-filter your model accordingly.
@property (nonatomic, nullable) NSString *filterText;
/// @return A new section containing only rows that match the string,
/// or nil if the section was empty and no rows matched the string.
- (nullable instancetype)newSectionWithRowsMatchingQuery:(NSString *)query;
/// Provides an avenue for the section to refresh data or change the number of rows.
///
/// This is called before reloading the table view itself. If your section pulls data
/// from an external data source, this is a good place to refresh that data entirely.
/// If your section does not, then it might be simpler for you to just override
/// \c setFilterText: to call \c super and call \c reloadData.
- (void)reloadData;
@end
#pragma mark - Row Selection
/// Whether the given row should be selectable, such as if tapping the cell
/// should take the user to a new screen or trigger an action.
/// Subclasses \e may override this as necessary, but are not required to.
/// @return \c NO by default
- (BOOL)canSelectRow:(NSInteger)row;
/// An action "future" to be triggered when the row is selected, if the row
/// supports being selected as indicated by \c canSelectRow:. Subclasses
/// must implement this in accordance with how they implement \c canSelectRow:
/// if they do not implement \c viewControllerToPushForRow:
/// @return This returns \c nil if no view controller is provided by
/// \c viewControllerToPushForRow: — otherwise it pushes that view controller
/// onto \c host.navigationController
- (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row;
/// A view controller to display when the row is selected, if the row
/// supports being selected as indicated by \c canSelectRow:. Subclasses
/// must implement this in accordance with how they implement \c canSelectRow:
/// if they do not implement \c didSelectRowAction:
/// @return \c nil by default
- (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row;
/// Called when the accessory view's detail button is pressed.
/// @return \c nil by default.
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
- (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
/// Protected, not intended for public use. \c menuTitleForRow:
/// already includes the value returned from this method.
///
/// By default, this returns \c @"". Subclasses may override to
/// provide a detailed description of the target of the context menu.
- (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
/// The context menu items, if any. Subclasses may override.
/// By default, only inludes items for \c copyMenuItemsForRow:.
- (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
/// Subclasses may override to return a list of copiable items.
///
/// Every two elements in the list compose a key-value pair, where the key
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
/// Provide a reuse identifier for the given row. Subclasses should override.
///
/// Custom reuse identifiers should be specified in \c cellRegistrationMapping.
/// You may return any of the identifiers in \c FLEXTableView.h
/// without including them in the \c cellRegistrationMapping.
/// @return \c kFLEXDefaultCell by default.
- (NSString *)reuseIdentifierForRow:(NSInteger)row;
/// Configure a cell for the given row. Subclasses must override.
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row;
#pragma mark - External Convenience
/// For use by whatever view controller uses your section. Not required.
/// @return An optional title.
- (nullable NSString *)titleForRow:(NSInteger)row;
/// For use by whatever view controller uses your section. Not required.
/// @return An optional subtitle.
- (nullable NSString *)subtitleForRow:(NSInteger)row;
@interface FLEXTableViewSection<__covariant ObjectType> (Subscripting)
- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
@end
NS_ASSUME_NONNULL_END
+109 -30
View File
@@ -2,48 +2,127 @@
// FLEXTableViewSection.m
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "UIMenu+FLEX.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation FLEXTableViewSection
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray *)rows {
FLEXTableViewSection *s = [self new];
s->_section = section;
s->_title = title;
s->_rows = rows.copy;
return s;
- (NSInteger)numberOfRows {
return 0;
}
- (instancetype)newSectionWithRowsMatchingQuery:(NSString *)query {
// Find rows containing the search string
NSPredicate *containsString = [NSPredicate predicateWithBlock:^BOOL(id<FLEXPatternMatching> obj, NSDictionary *bindings) {
return [obj matches:query];
}];
NSArray *filteredRows = [self.rows filteredArrayUsingPredicate:containsString];
// Only return new section if not empty
if (filteredRows.count) {
return [[self class] section:self.section title:self.title rows:filteredRows];
}
- (void)reloadData { }
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
return nil;
}
- (NSInteger)count {
return self.rows.count;
- (BOOL)canSelectRow:(NSInteger)row { return NO; }
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
UIViewController *toPush = [self viewControllerToPushForRow:row];
if (toPush) {
return ^(UIViewController *host) {
[host.navigationController pushViewController:toPush animated:YES];
};
}
return nil;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return nil;
}
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
return nil;
}
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
if (subtitle.length) {
return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle];
}
return title;
}
- (NSString *)menuSubtitleForRow:(NSInteger)row {
return @"";
}
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender 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
@implementation FLEXTableViewSection (Subscripting)
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
return self.rows[idx];
}
@end
#pragma clang diagnostic pop
@@ -25,8 +25,10 @@
_selectionIndicatorStripe = [UIView new];
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.titleLabel.adjustsFontForContentSizeCategory = YES;
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
if (@available(iOS 10, *)) {
self.titleLabel.adjustsFontForContentSizeCategory = YES;
}
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.selectionIndicatorStripe];
@@ -45,7 +47,7 @@
if (self.selected) {
self.titleLabel.textColor = self.tintColor;
} else {
self.titleLabel.textColor = [FLEXColor deemphasizedTextColor];
self.titleLabel.textColor = FLEXColor.deemphasizedTextColor;
}
}
@@ -17,7 +17,6 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, readonly) UICollectionView *collectionView;
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
@property (nonatomic, readonly) NSLayoutConstraint *heightConstraint;
@property (nonatomic, readonly) id dynamicTypeObserver;
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
@@ -30,8 +29,9 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [FLEXColor primaryBackgroundColor];
self.backgroundColor = FLEXColor.primaryBackgroundColor;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.translatesAutoresizingMaskIntoConstraints = YES;
_dynamicTypeHandlers = [NSMutableArray new];
CGSize itemSize = CGSizeZero;
@@ -104,7 +104,7 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
// Draw hairline
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [FLEXColor hairlineColor].CGColor);
CGContextSetStrokeColorWithColor(context, FLEXColor.hairlineColor.CGColor);
CGContextSetLineWidth(context, width);
CGContextMoveToPoint(context, 0, rect.size.height - width);
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
@@ -117,25 +117,22 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor].active = YES;
[self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor].active = YES;
[self.topAnchor constraintEqualToAnchor:self.superview.topAnchor].active = YES;
[self.collectionView pinEdgesToSuperview];
_heightConstraint = [self.heightAnchor constraintEqualToConstant:100];
self.heightConstraint.active = YES;
self.constraintsInstalled = YES;
}
self.heightConstraint.constant = [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[super updateConstraints];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(
UIViewNoIntrinsicMetric,
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
);
}
#pragma mark - Public
- (void)setItems:(NSArray<NSString *> *)items {
@@ -0,0 +1,17 @@
//
// FLEXCodeFontCell.h
// FLEX
//
// Created by Tanner on 12/27/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXMultilineTableViewCell.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
//
// FLEXCodeFontCell.m
// FLEX
//
// Created by Tanner on 12/27/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXCodeFontCell.h"
#import "UIFont+FLEX.h"
@implementation FLEXCodeFontCell
- (void)postInit {
[super postInit];
self.titleLabel.font = UIFont.flex_codeFont;
self.subtitleLabel.font = UIFont.flex_codeFont;
self.titleLabel.adjustsFontSizeToFitWidth = YES;
self.titleLabel.minimumScaleFactor = 0.9;
self.subtitleLabel.adjustsFontSizeToFitWidth = YES;
self.subtitleLabel.minimumScaleFactor = 0.75;
// Disable mutli-line pre iOS 11
if (@available(iOS 11, *)) {
self.subtitleLabel.numberOfLines = 5;
} else {
self.titleLabel.numberOfLines = 1;
self.subtitleLabel.numberOfLines = 1;
}
}
@end
@@ -0,0 +1,13 @@
//
// FLEXKeyValueTableViewCell.h
// FLEX
//
// Created by Tanner Bennett on 1/23/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
@interface FLEXKeyValueTableViewCell : FLEXTableViewCell
@end
@@ -0,0 +1,17 @@
//
// FLEXKeyValueTableViewCell.m
// FLEX
//
// Created by Tanner Bennett on 1/23/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXKeyValueTableViewCell.h"
@implementation FLEXKeyValueTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
}
@end
@@ -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
@@ -10,8 +10,7 @@
@implementation FLEXSubtitleTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@@ -0,0 +1,23 @@
//
// FLEXTableViewCell.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableViewCell : UITableViewCell
/// Use this instead of .textLabel
@property (nonatomic, readonly) UILabel *titleLabel;
/// Use this instead of .detailTextLabel
@property (nonatomic, readonly) UILabel *subtitleLabel;
/// Subclasses can override this instead of initializers to
/// perform additional initialization without lots of boilerplate.
/// Remember to call super!
- (void)postInit;
@end
@@ -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"
@@ -30,18 +30,16 @@
@implementation FLEXColorComponentInputView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.slider = [UISlider new];
self.slider.backgroundColor = self.backgroundColor;
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.slider];
self.valueLabel = [UILabel new];
self.valueLabel.backgroundColor = self.backgroundColor;
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
self.valueLabel.font = [UIFont systemFontOfSize:14.0];
self.valueLabel.textAlignment = NSTextAlignmentRight;
[self addSubview:self.valueLabel];
@@ -50,15 +48,13 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.slider.backgroundColor = backgroundColor;
self.valueLabel.backgroundColor = backgroundColor;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
const CGFloat kValueLabelWidth = 50.0;
@@ -73,19 +69,16 @@
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
}
- (void)sliderChanged:(id)sender
{
- (void)sliderChanged:(id)sender {
[self.delegate colorComponentInputViewValueDidChange:self];
[self updateValueLabel];
}
- (void)updateValueLabel
{
- (void)updateValueLabel {
self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = [self.slider sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
@@ -102,8 +95,7 @@
@implementation FLEXColorPreviewBox
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1.0;
@@ -118,18 +110,15 @@
return self;
}
- (void)setColor:(UIColor *)color
{
- (void)setColor:(UIColor *)color {
self.colorOverlayView.backgroundColor = color;
}
- (UIColor *)color
{
- (UIColor *)color {
return self.colorOverlayView.backgroundColor;
}
+ (UIImage *)backgroundPatternImage
{
+ (UIImage *)backgroundPatternImage {
const CGFloat kSquareDimension = 5.0;
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
@@ -164,8 +153,7 @@
@implementation FLEXArgumentInputColorView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.colorPreviewBox = [FLEXColorPreviewBox new];
@@ -174,7 +162,7 @@
self.hexLabel = [UILabel new];
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
self.hexLabel.textAlignment = NSTextAlignmentCenter;
self.hexLabel.font = [FLEXUtility defaultFontOfSize:12.0];
self.hexLabel.font = [UIFont systemFontOfSize:12.0];
[self addSubview:self.hexLabel];
self.alphaInput = [FLEXColorComponentInputView new];
@@ -200,8 +188,7 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.alphaInput.backgroundColor = backgroundColor;
self.redInput.backgroundColor = backgroundColor;
@@ -209,8 +196,7 @@
self.blueInput.backgroundColor = backgroundColor;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = 0;
@@ -236,8 +222,7 @@
}
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[UIColor class]]) {
[self updateWithColor:inputValue];
} else if ([inputValue isKindOfClass:[NSValue class]]) {
@@ -248,21 +233,20 @@
UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
[self updateWithColor:color];
}
} else {
[self updateWithColor:UIColor.clearColor];
}
}
- (id)inputValue
{
- (id)inputValue {
return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value];
}
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView
{
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView {
[self updateColorPreview];
}
- (void)updateWithColor:(UIColor *)color
{
- (void)updateWithColor:(UIColor *)color {
CGFloat red, green, blue, white, alpha;
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
self.alphaInput.slider.value = alpha;
@@ -286,8 +270,7 @@
[self updateColorPreview];
}
- (void)updateColorPreview
{
- (void)updateColorPreview {
self.colorPreviewBox.color = self.inputValue;
unsigned char redByte = self.redInput.slider.value * 255;
unsigned char greenByte = self.greenInput.slider.value * 255;
@@ -296,8 +279,7 @@
[self setNeedsLayout];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = 0;
height += [[self class] colorPreviewBoxHeight];
height += [[self class] inputViewVerticalPadding];
@@ -311,19 +293,19 @@
return CGSizeMake(size.width, height);
}
+ (CGFloat)inputViewVerticalPadding
{
+ (CGFloat)inputViewVerticalPadding {
return 10.0;
}
+ (CGFloat)colorPreviewBoxHeight
{
+ (CGFloat)colorPreviewBoxHeight {
return 40.0;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, @encode(CGColorRef)) == 0 || strcmp(type, FLEXEncodeClass(UIColor)) == 0)) || [value isKindOfClass:[UIColor class]];
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// We don't care if currentValue is a color or not; we will default to +clearColor
return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0);
}
@end
@@ -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"
@@ -17,8 +17,7 @@
@implementation FLEXArgumentInputDateView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.datePicker = [UIDatePicker new];
@@ -31,33 +30,29 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSDate class]]) {
self.datePicker.date = inputValue;
}
}
- (id)inputValue
{
- (id)inputValue {
return self.datePicker.date;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.datePicker.frame = self.bounds;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = [self.datePicker sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, FLEXEncodeClass(NSDate)) == 0)) || [value isKindOfClass:[NSDate class]];
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
return strcmp(type, FLEXEncodeClass(NSDate)) == 0;
}
@end
@@ -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"
@@ -20,18 +20,15 @@
@implementation FLEXArgumentInputFontView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
self.fontNameInput.backgroundColor = self.backgroundColor;
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.fontNameInput.title = @"Font Name:";
[self addSubview:self.fontNameInput];
self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)];
self.pointSizeInput.backgroundColor = self.backgroundColor;
self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.pointSizeInput.title = @"Point Size:";
[self addSubview:self.pointSizeInput];
@@ -39,15 +36,13 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.fontNameInput.backgroundColor = backgroundColor;
self.pointSizeInput.backgroundColor = backgroundColor;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[UIFont class]]) {
UIFont *font = (UIFont *)inputValue;
self.fontNameInput.inputValue = font.fontName;
@@ -55,8 +50,7 @@
}
}
- (id)inputValue
{
- (id)inputValue {
CGFloat pointSize = 0;
if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) {
NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue;
@@ -67,16 +61,14 @@
return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize];
}
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder];
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
@@ -89,13 +81,11 @@
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
}
+ (CGFloat)verticalPaddingBetweenFields
{
+ (CGFloat)verticalPaddingBetweenFields {
return 10.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
@@ -111,11 +101,9 @@
#pragma mark -
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
BOOL supported = type && strcmp(type, FLEXEncodeClass(UIFont)) == 0;
supported = supported || (value && [value isKindOfClass:[UIFont class]]);
return supported;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
return strcmp(type, FLEXEncodeClass(UIFont)) == 0;
}
@end
@@ -18,8 +18,7 @@
@implementation FLEXArgumentInputFontsPickerView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
@@ -29,8 +28,7 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
self.inputTextView.text = inputValue;
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
[self.availableFonts insertObject:inputValue atIndex:0];
@@ -38,15 +36,13 @@
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
}
- (id)inputValue
{
- (id)inputValue {
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
#pragma mark - private
- (UIPickerView*)createFontsPicker
{
- (UIPickerView*)createFontsPicker {
UIPickerView *fontsPicker = [UIPickerView new];
fontsPicker.dataSource = self;
fontsPicker.delegate = self;
@@ -54,10 +50,9 @@
return fontsPicker;
}
- (void)createAvailableFonts
{
NSMutableArray<NSString *> *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,20 +62,17 @@
#pragma mark - UIPickerViewDataSource
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.availableFonts.count;
}
#pragma mark - UIPickerViewDelegate
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UILabel *fontLabel;
if (!view) {
fontLabel = [UILabel new];
@@ -97,8 +89,7 @@
return fontLabel;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
self.inputTextView.text = self.availableFonts[row];
}
@@ -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,48 +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
{
- (id)inputValue {
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
static NSArray<NSString *> *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)),
@(@encode(long 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
@@ -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 "FLEXArgumentInputObjectView.h"
@@ -25,8 +25,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
@implementation FLEXArgumentInputObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
@@ -46,8 +45,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
return self;
}
- (void)didChangeType
{
- (void)didChangeType {
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
if (super.inputValue) {
@@ -61,8 +59,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
}
}
- (void)setInputType:(FLEXArgInputObjectType)inputType
{
- (void)setInputType:(FLEXArgInputObjectType)inputType {
if (_inputType == inputType) return;
_inputType = inputType;
@@ -98,14 +95,12 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
[self.superview setNeedsLayout];
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
super.inputValue = inputValue;
[self populateTextAreaFromValue:inputValue];
}
- (id)inputValue
{
- (id)inputValue {
switch (self.inputType) {
case FLEXArgInputObjectTypeJSON:
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
@@ -122,8 +117,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
}
}
- (void)populateTextAreaFromValue:(id)value
{
- (void)populateTextAreaFromValue:(id)value {
if (!value) {
self.inputTextView.text = nil;
} else {
@@ -138,16 +132,14 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
[self textViewDidChange:self.inputTextView];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
return fitSize;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
self.objectTypeSegmentControl.frame = CGRectMake(
0.0,
@@ -162,23 +154,20 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
[super layoutSubviews];
}
- (CGFloat)topInputFieldVerticalLayoutGuide
{
- (CGFloat)topInputFieldVerticalLayoutGuide {
// Our text view is offset from the segmented control
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// Must be object type
return type[0] == '@';
return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
}
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value
{
NSParameterAssert(type[0] == '@');
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
if (value) {
// If there's a current value, it must be serializable to JSON
@@ -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,36 +11,119 @@
@implementation FLEXArgumentInputStringView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeLarge;
FLEXTypeEncoding type = typeEncoding[0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = typeEncoding[1];
}
// Selectors don't need a multi-line text box
if (type == FLEXTypeEncodingSelector) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
} else {
self.targetSize = FLEXArgumentInputViewSizeLarge;
}
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = inputValue;
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSString class]]) {
self.inputTextView.text = inputValue;
} else if ([inputValue isKindOfClass:[NSValue class]]) {
NSValue *value = (id)inputValue;
NSParameterAssert(strlen(value.objCType) == 1);
// C-String or SEL from NSValue
FLEXTypeEncoding type = value.objCType[0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = value.objCType[1];
}
if (type == FLEXTypeEncodingCString) {
self.inputTextView.text = @((const char *)value.pointerValue);
} else if (type == FLEXTypeEncodingSelector) {
self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue);
}
}
}
- (id)inputValue
{
- (id)inputValue {
NSString *text = self.inputTextView.text;
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
// but we accept that tradeoff in exchange for not having to type quotes for every string.
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
if (!text.length) {
return nil;
}
// Case: C-strings and SELs
if (self.typeEncoding.length <= 2) {
FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = [self.typeEncoding characterAtIndex:1];
}
if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) {
const char *encoding = self.typeEncoding.UTF8String;
SEL selector = NSSelectorFromString(text);
return [NSValue valueWithBytes:&selector objCType:encoding];
}
}
// Case: NSStrings
return self.inputTextView.text.copy;
}
// TODO: Support using object address for strings, as in the object arg view.
#pragma mark -
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
unsigned long len = strlen(type);
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
BOOL supported = type && strcmp(type, FLEXEncodeClass(NSString)) == 0;
supported = supported || (value && [value isKindOfClass:[NSString class]]);
return supported;
BOOL isConst = type[0] == FLEXTypeEncodingConst;
NSInteger i = isConst ? 1 : 0;
BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0;
BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString;
BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector;
BOOL valueIsString = [value isKindOfClass:[NSString class]];
BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString;
BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL;
BOOL valueIsNSValueWithCorrectType = NO;
if ([value isKindOfClass:[NSValue class]]) {
NSValue *v = (id)value;
len = strlen(v.objCType);
if (len == 1) {
FLEXTypeEncoding type = v.objCType[i];
if (type == FLEXTypeEncodingCString && typeIsCString) {
valueIsNSValueWithCorrectType = YES;
} else if (type == FLEXTypeEncodingSelector && typeIsSEL) {
valueIsNSValueWithCorrectType = YES;
}
}
}
if (!value && typeIsSupported) {
return YES;
}
if (typeIsString && valueIsString) {
return YES;
}
// Primitive strings can be input as NSStrings or NSValues
if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) {
return YES;
}
return NO;
}
@end
@@ -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,12 +3,13 @@
// 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 ()
@@ -18,22 +19,26 @@
@implementation FLEXArgumentInputStructView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray array];
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) {
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
inputView.backgroundColor = self.backgroundColor;
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
if (fieldIndex < customTitles.count) {
inputView.title = customTitles[fieldIndex];
} else {
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)",
structName, (unsigned long)fieldIndex, prettyTypeEncoding
];
}
[inputViews addObject:inputView];
@@ -47,29 +52,27 @@
#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) {
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[fieldIndex];
@@ -87,19 +90,18 @@
}
}
- (id)inputValue
{
- (id)inputValue {
NSValue *boxedStruct = nil;
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[fieldIndex];
@@ -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,13 +172,16 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return type && type[0] == FLEXTypeEncodingStructBegin;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
if (type[0] == FLEXTypeEncodingStructBegin) {
return FLEXGetSizeAndAlignment(type, nil, nil);
}
return NO;
}
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
{
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
NSArray<NSString *> *customTitles = nil;
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
customTitles = @[@"CGPoint origin", @"CGSize size"];
@@ -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,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 "FLEXArgumentInputSwitchView.h"
@@ -16,8 +16,7 @@
@implementation FLEXArgumentInputSwitchView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputSwitch = [UISwitch new];
@@ -31,8 +30,7 @@
#pragma mark Input/Output
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
BOOL on = NO;
if ([inputValue isKindOfClass:[NSNumber class]]) {
NSNumber *number = (NSNumber *)inputValue;
@@ -46,30 +44,26 @@
self.inputSwitch.on = on;
}
- (id)inputValue
{
- (id)inputValue {
BOOL isOn = [self.inputSwitch isOn];
NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)];
return boxedBool;
}
- (void)switchValueDidChange:(id)sender
{
- (void)switchValueDidChange:(id)sender {
[self.delegate argumentInputViewValueDidChange:self];
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += self.inputSwitch.frame.size.height;
return fitSize;
@@ -78,10 +72,10 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// Only BOOLs. Current value is irrelevant.
return type && strcmp(type, @encode(BOOL)) == 0;
return strcmp(type, @encode(BOOL)) == 0;
}
@end
@@ -20,26 +20,31 @@
@implementation FLEXArgumentInputTextView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView = [UITextView new];
self.inputTextView.font = [[self class] inputFont];
self.inputTextView.backgroundColor = [FLEXColor primaryBackgroundColor];
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.cornerRadius = 5.f;
self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
self.inputTextView.layer.cornerRadius = 10.f;
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
[self addSubview:self.inputTextView];
if (@available(iOS 11, *)) {
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor;
}
self.placeholderLabel = [UILabel new];
self.placeholderLabel.font = self.inputTextView.font;
self.placeholderLabel.textColor = [FLEXColor deemphasizedTextColor];
self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor;
self.placeholderLabel.numberOfLines = 0;
[self addSubview:self.inputTextView];
[self.inputTextView addSubview:self.placeholderLabel];
}
@@ -48,23 +53,26 @@
#pragma mark - Private
- (UIToolbar *)createToolBar
{
- (UIToolbar *)createToolBar {
UIToolbar *toolBar = [UIToolbar new];
[toolBar sizeToFit];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(textViewDone)];
toolBar.items = @[spaceItem, doneItem];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil action:nil
];
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
target:self.inputTextView action:@selector(paste:)
];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self.inputTextView action:@selector(resignFirstResponder)
];
toolBar.items = @[spaceItem, pasteItem, doneItem];
return toolBar;
}
- (void)textViewDone
{
[self.inputTextView resignFirstResponder];
}
- (void)setInputPlaceholderText:(NSString *)placeholder
{
- (void)setInputPlaceholderText:(NSString *)placeholder {
self.placeholderLabel.text = placeholder;
if (placeholder.length) {
if (!self.inputTextView.text.length) {
@@ -79,41 +87,35 @@
[self setNeedsLayout];
}
- (NSString *)inputPlaceholderText
{
- (NSString *)inputPlaceholderText {
return self.placeholderLabel.text;
}
#pragma mark - Superclass Overrides
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return self.inputTextView.isFirstResponder;
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
// Placeholder label is positioned by insetting origin,
// which is the line fragment padding for X and 0 for Y,
// Placeholder label is positioned by insetting then origin
// by the content inset then the text container inset
CGFloat leading = self.inputTextView.textContainer.lineFragmentPadding;
CGSize s = self.inputTextView.frame.size;
self.placeholderLabel.frame = CGRectMake(leading, 0, s.width, s.height);
self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height);
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
self.inputTextView.textContainerInset
);
}
- (NSUInteger)numberOfInputLines
{
- (NSUInteger)numberOfInputLines {
switch (self.targetSize) {
case FLEXArgumentInputViewSizeDefault:
return 2;
@@ -124,45 +126,27 @@
}
}
- (CGFloat)inputTextViewHeight
{
- (CGFloat)inputTextViewHeight {
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self inputTextViewHeight];
return fitSize;
}
#pragma mark - Trait collection changes
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
}
}
#endif
}
#pragma mark - Class Helpers
+ (UIFont *)inputFont
{
return [FLEXUtility defaultFontOfSize:14.0];
+ (UIFont *)inputFont {
return [UIFont systemFontOfSize:14.0];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView
{
- (void)textViewDidChange:(UITextView *)textView {
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
}
@@ -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
};
@@ -3,11 +3,12 @@
// 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 ()
@@ -18,8 +19,7 @@
@implementation FLEXArgumentInputView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithFrame:CGRectZero];
if (self) {
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
@@ -27,8 +27,7 @@
return self;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
if (self.showsTitle) {
@@ -38,14 +37,12 @@
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.titleLabel.backgroundColor = backgroundColor;
}
- (void)setTitle:(NSString *)title
{
- (void)setTitle:(NSString *)title {
if (![_title isEqual:title]) {
_title = title;
self.titleLabel.text = title;
@@ -53,26 +50,22 @@
}
}
- (UILabel *)titleLabel
{
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [UILabel new];
_titleLabel.font = [[self class] titleFont];
_titleLabel.backgroundColor = self.backgroundColor;
_titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
_titleLabel.textColor = FLEXColor.primaryTextColor;
_titleLabel.numberOfLines = 0;
[self addSubview:_titleLabel];
}
return _titleLabel;
}
- (BOOL)showsTitle
{
- (BOOL)showsTitle {
return self.title.length > 0;
}
- (CGFloat)topInputFieldVerticalLayoutGuide
{
- (CGFloat)topInputFieldVerticalLayoutGuide {
CGFloat verticalLayoutGuide = 0;
if (self.showsTitle) {
CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height;
@@ -84,34 +77,29 @@
#pragma mark - Subclasses Can Override
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return NO;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
return NO;
}
#pragma mark - Class Helpers
+ (UIFont *)titleFont
{
return [FLEXUtility defaultFontOfSize:12.0];
+ (UIFont *)titleFont {
return [UIFont systemFontOfSize:12.0];
}
+ (CGFloat)titleBottomPadding
{
+ (CGFloat)titleBottomPadding {
return 4.0;
}
#pragma mark - Sizing
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = 0;
if (self.title.length > 0) {
@@ -7,8 +7,7 @@
//
#import <Foundation/Foundation.h>
@class FLEXArgumentInputView;
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXArgumentInputViewFactory : NSObject
@@ -21,13 +21,11 @@
@implementation FLEXArgumentInputViewFactory
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
{
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding {
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
}
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
if (!subclass) {
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
@@ -39,8 +37,7 @@
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset];
}
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
Class argumentInputViewSubclass = nil;
@@ -66,8 +63,7 @@
return argumentInputViewSubclass;
}
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
}
@@ -3,12 +3,12 @@
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import "FLEXFieldEditorViewController.h"
@interface FLEXDefaultEditorViewController : FLEXMutableFieldEditorViewController
@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
@@ -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"
@@ -21,8 +21,7 @@
@implementation FLEXDefaultEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key
{
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key {
self = [super initWithTarget:defaults];
if (self) {
self.key = key;
@@ -31,13 +30,11 @@
return self;
}
- (NSUserDefaults *)defaults
{
- (NSUserDefaults *)defaults {
return [self.target isKindOfClass:[NSUserDefaults class]] ? self.target : nil;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.fieldEditorView.fieldDescription = self.key;
@@ -52,8 +49,7 @@
self.fieldEditorView.argumentInputViews = @[inputView];
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
id value = self.firstInputView.inputValue;
@@ -67,15 +63,13 @@
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
}
- (void)getterButtonPressed:(id)sender
{
- (void)getterButtonPressed:(id)sender {
[super getterButtonPressed:sender];
id returnedObject = [self.defaults objectForKey:self.key];
[self exploreObjectOrPopViewController:returnedObject];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue
{
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
return [FLEXArgumentInputViewFactory
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
+2 -2
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 <UIKit/UIKit.h>
@@ -15,6 +15,6 @@
@property (nonatomic, copy) NSString *targetDescription;
@property (nonatomic, copy) NSString *fieldDescription;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
+15 -28
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"
@@ -21,8 +21,7 @@
@implementation FLEXFieldEditorView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.targetDescriptionLabel = [UILabel new];
@@ -44,8 +43,7 @@
return self;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat horizontalPadding = [[self class] horizontalPadding];
@@ -78,15 +76,13 @@
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.targetDescriptionLabel.backgroundColor = backgroundColor;
self.fieldDescriptionLabel.backgroundColor = backgroundColor;
}
- (void)setTargetDescription:(NSString *)targetDescription
{
- (void)setTargetDescription:(NSString *)targetDescription {
if (![_targetDescription isEqual:targetDescription]) {
_targetDescription = targetDescription;
self.targetDescriptionLabel.text = targetDescription;
@@ -94,8 +90,7 @@
}
}
- (void)setFieldDescription:(NSString *)fieldDescription
{
- (void)setFieldDescription:(NSString *)fieldDescription {
if (![_fieldDescription isEqual:fieldDescription]) {
_fieldDescription = fieldDescription;
self.fieldDescriptionLabel.text = fieldDescription;
@@ -103,8 +98,7 @@
}
}
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews
{
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
if (![_argumentInputViews isEqual:argumentInputViews]) {
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
@@ -121,40 +115,33 @@
}
}
+ (UIView *)dividerView
{
+ (UIView *)dividerView {
UIView *dividerView = [UIView new];
dividerView.backgroundColor = [self dividerColor];
return dividerView;
}
+ (UIColor *)dividerColor
{
+ (UIColor *)dividerColor {
return UIColor.lightGrayColor;
}
+ (CGFloat)horizontalPadding
{
+ (CGFloat)horizontalPadding {
return 10.0;
}
+ (CGFloat)verticalPadding
{
+ (CGFloat)verticalPadding {
return 20.0;
}
+ (UIFont *)labelFont
{
return [FLEXUtility defaultFontOfSize:14.0];
+ (UIFont *)labelFont {
return [UIFont systemFontOfSize:14.0];
}
+ (CGFloat)dividerLineHeight
{
+ (CGFloat)dividerLineHeight {
return 1.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat horizontalPadding = [[self class] horizontalPadding];
CGFloat verticalPadding = [[self class] verticalPadding];
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
+17 -20
View File
@@ -1,32 +1,29 @@
//
// FLEXFieldEditorViewController.h
// Flipboard
// FLEX
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXVariableEditorViewController.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
@class FLEXFieldEditorView;
@class FLEXArgumentInputView;
NS_ASSUME_NONNULL_BEGIN
@interface FLEXFieldEditorViewController : UIViewController
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
- (id)initWithTarget:(id)target;
/// @return nil if the property is readonly or if the type is unsupported
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property;
/// @return nil if the ivar type is unsupported
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar;
// Convenience accessor since many subclasses only use one input view
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
/// Subclasses can change the button title via the \c title property
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
// For subclass use only.
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
- (NSString *)titleForActionButton;
/// Pushes an explorer view controller for the given object
/// or pops the current view controller.
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
- (void)getterButtonPressed:(id)sender;
@end
NS_ASSUME_NONNULL_END
+131 -100
View File
@@ -1,132 +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 "FLEXColor.h"
#import "FLEXFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) FLEXProperty *property;
@property (nonatomic) FLEXIvar *ivar;
@property (nonatomic, readwrite) id target;
@property (nonatomic, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readwrite) UIBarButtonItem *setterButton;
@property (nonatomic, readonly) id currentValue;
@property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding;
@property (nonatomic, readonly) NSString *fieldDescription;
@end
@implementation FLEXFieldEditorViewController
- (id)initWithTarget:(id)target
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.target = target;
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
#pragma mark - Initialization
+ (instancetype)target:(id)target property:(FLEXProperty *)property {
id value = [property getValue:target];
if (![self canEditProperty:property onObject:target currentValue:value]) {
return nil;
}
return self;
FLEXFieldEditorViewController *editor = [self target:target];
editor.title = [@"Property: " 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 = [FLEXColor scrollViewBackgroundColor];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
self.fieldEditorView = [FLEXFieldEditorView new];
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
self.navigationItem.rightBarButtonItem = self.setterButton;
}
- (void)viewWillLayoutSubviews
{
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
self.scrollView.contentSize = fieldEditorSize;
}
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
- (FLEXArgumentInputView *)firstInputView
{
return [self.fieldEditorView argumentInputViews].firstObject;
}
// Create getter button
_getterButton = [[UIBarButtonItem alloc]
initWithTitle:@"Get"
style:UIBarButtonItemStyleDone
target:self
action:@selector(getterButtonPressed:)
];
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
];
- (void)actionButtonPressed:(id)sender
{
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
// Configure input view
self.fieldEditorView.fieldDescription = self.fieldDescription;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
inputView.inputValue = self.currentValue;
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
- (NSString *)titleForActionButton
{
// Subclasses can override.
return @"Set";
}
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
if (objectOrNil) {
// For non-nil (or void) return types, push an explorer view controller to display the object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
// Don't show a "set" button for switches; we mutate when the switch is flipped
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.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)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
if (self.property) {
id userInputObject = self.firstInputView.inputValue;
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
SEL setterSelector = self.property.likelySetter;
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
[FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self];
sender = nil; // Don't pop back
}
} else {
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[self.ivar setValue:self.firstInputView.inputValue onObject:self.target];
}
// Go back after setting, but not for switches.
if (sender) {
[self.navigationController popViewControllerAnimated:YES];
} else {
self.firstInputView.inputValue = self.currentValue;
}
}
- (void)getterButtonPressed:(id)sender {
[self.fieldEditorView endEditing:YES];
[self exploreObjectOrPopViewController:self.currentValue];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
#pragma mark - Private
- (id)currentValue {
if (self.property) {
return [self.property getValue:self.target];
} else {
return [self.ivar getValue:self.target];
}
}
- (const FLEXTypeEncoding *)typeEncoding {
if (self.property) {
return self.property.attributes.typeEncoding.UTF8String;
} else {
return self.ivar.typeEncoding.UTF8String;
}
}
- (NSString *)fieldDescription {
if (self.property) {
return self.property.fullDescription;
} else {
return self.ivar.description;
}
}
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
return canEditType && [object respondsToSelector:property.likelySetter];
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -1,18 +0,0 @@
//
// FLEXIvarEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXIvarEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
@end
@@ -1,86 +0,0 @@
//
// FLEXIvarEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXIvarEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic) Ivar ivar;
@end
@implementation FLEXIvarEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar
{
self = [super initWithTarget:target];
if (self) {
self.ivar = ivar;
self.title = @"Instance Variable";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:ivar_getTypeEncoding(self.ivar)];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches. Set the ivar when the switch toggles.
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
// Pop view controller for consistency;
// property setters and method calls also pop on success.
[self.navigationController popViewControllerAnimated:YES];
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -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"
@@ -16,93 +16,90 @@
#import "FLEXUtility.h"
@interface FLEXMethodCallingViewController ()
@property (nonatomic) Method method;
@property (nonatomic) FLEXTypeEncoding *returnType;
@property (nonatomic) FLEXMethod *method;
@end
@implementation FLEXMethodCallingViewController
- (id)initWithTarget:(id)target method:(Method)method
{
+ (instancetype)target:(id)target method:(FLEXMethod *)method {
return [[self alloc] initWithTarget:target method:method];
}
- (id)initWithTarget:(id)target method:(FLEXMethod *)method {
NSParameterAssert(method.isInstanceMethod == !object_isClass(target));
self = [super initWithTarget:target];
if (self) {
self.method = method;
self.returnType = [FLEXRuntimeUtility returnTypeForMethod:method];
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: ";
self.title = [self.title stringByAppendingString:method.selectorString];
}
return self;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
NSString *returnType = @((const char *)self.returnType);
NSString *methodDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSString *format = @"Signature:\n%@\n\nReturn Type:\n%@";
NSString *info = [NSString stringWithFormat:format, methodDescription, returnType];
self.fieldEditorView.fieldDescription = info;
NSArray<NSString *> *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
NSMutableArray<FLEXArgumentInputView *> *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;
}
- (void)dealloc
{
free(self.returnType);
self.returnType = NULL;
}
- (BOOL)isClassMethod
{
return self.target && self.target == [self.target class];
}
- (NSString *)titleForActionButton
{
return @"Call";
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
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) {
[FLEXAlert showAlert:@"Method Call Failed" message:[error localizedDescription] from:self];
} else if (returnedObject) {
[FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self];
} else if (returnValue) {
// For non-nil (or void) return types, push an explorer view controller to display the returned object
returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
[self.navigationController pushViewController:explorerViewController animated:YES];
returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType];
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue];
[self.navigationController pushViewController:explorer animated:YES];
} else {
[self exploreObjectOrPopViewController:returnedObject];
[self exploreObjectOrPopViewController:returnValue];
}
}
@@ -1,18 +0,0 @@
//
// FLEXMutableFieldEditorViewController.h
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
@interface FLEXMutableFieldEditorViewController : FLEXFieldEditorViewController
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
- (void)getterButtonPressed:(id)sender;
- (NSString *)titleForGetterButton;
@end
@@ -1,36 +0,0 @@
//
// FLEXMutableFieldEditorViewController.m
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
@interface FLEXMutableFieldEditorViewController ()
@property (nonatomic, readwrite) UIBarButtonItem *getterButton;
@end
@implementation FLEXMutableFieldEditorViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.getterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForGetterButton] style:UIBarButtonItemStyleDone target:self action:@selector(getterButtonPressed:)];
self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
}
- (void)getterButtonPressed:(id)sender {
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
- (NSString *)titleForGetterButton {
return @"Get";
}
@end
@@ -1,18 +0,0 @@
//
// FLEXPropertyEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXPropertyEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property;
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value;
@end
@@ -1,100 +0,0 @@
//
// FLEXPropertyEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXPropertyEditorViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXUtility.h"
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic) objc_property_t property;
@end
@implementation FLEXPropertyEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property
{
self = [super initWithTarget:target];
if (self) {
self.property = property;
self.title = @"Property";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
self.setterButton.enabled = [[self class] canEditProperty:self.property onObject:self.target currentValue:currentValue];
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:self.property].UTF8String;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches - just call the setter immediately after the switch toggles.
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
id userInputObject = self.firstInputView.inputValue;
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
[FLEXAlert showAlert:@"Property Setter Failed" message:[error localizedDescription] from:self];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
} else {
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
// Don't do this for simulated taps on the action button (i.e. from switch/BOOL editors). The experience is weird there.
if (sender) {
[self.navigationController popViewControllerAnimated:YES];
}
}
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value
{
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:property].UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:property];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property] && (!setterSelector || ![object respondsToSelector:setterSelector]);
return canEditType && !isReadonly;
}
@end
@@ -0,0 +1,35 @@
//
// FLEXVariableEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 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
@@ -3,26 +3,29 @@
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#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;
- (BOOL)wantsWindowToBecomeKey;
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UIViewController *(^)(void))future completion:(void(^)(void))completion;
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
// Keyboard shortcut helpers
@@ -30,15 +33,19 @@
- (void)toggleMoveTool;
- (void)toggleViewsTool;
- (void)toggleMenuTool;
- (void)handleDownArrowKeyPressed;
- (void)handleUpArrowKeyPressed;
- (void)handleRightArrowKeyPressed;
- (void)handleLeftArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleDownArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleUpArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleRightArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleLeftArrowKeyPressed;
@end
#pragma mark -
@protocol FLEXExplorerViewControllerDelegate <NSObject>
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
@end
File diff suppressed because it is too large Load Diff
@@ -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
+14 -9
View File
@@ -3,22 +3,27 @@
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Copyright (c) 2020 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
#pragma mark -
@interface FLEXWindow : UIWindow
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture input.
/// If we're just showing the toolbar, we want the main app's window to remain key
/// so that we don't interfere with input, status bar, etc.
@property (nonatomic, readonly) UIWindow *previousKeyWindow;
@end
+17 -12
View File
@@ -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,18 +32,25 @@
return pointInside;
}
- (BOOL)shouldAffectStatusBarAppearance
{
- (BOOL)shouldAffectStatusBarAppearance {
return [self isKeyWindow];
}
- (BOOL)canBecomeKeyWindow
{
- (BOOL)canBecomeKeyWindow {
return [self.eventDelegate canBecomeKeyWindow];
}
+ (void)initialize
{
- (void)makeKeyWindow {
_previousKeyWindow = FLEXUtility.appKeyWindow;
[super makeKeyWindow];
}
- (void)resignKeyWindow {
[super resignKeyWindow];
_previousKeyWindow = nil;
}
+ (void)initialize {
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
@@ -0,0 +1,17 @@
//
// FLEXWindowManagerController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXWindowManagerController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,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
+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>
+25
View File
@@ -0,0 +1,25 @@
//
// FLEX-Runtime.h
// FLEX
//
// Created by Tanner on 3/11/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <FLEX/FLEXObjcInternal.h>
#import <FLEX/FLEXRuntimeSafety.h>
#import <FLEX/FLEXBlockDescription.h>
#import <FLEX/FLEXTypeEncodingParser.h>
#import <FLEX/FLEXMirror.h>
#import <FLEX/FLEXProtocol.h>
#import <FLEX/FLEXProperty.h>
#import <FLEX/FLEXIvar.h>
#import <FLEX/FLEXMethodBase.h>
#import <FLEX/FLEXMethod.h>
#import <FLEX/FLEXPropertyAttributes.h>
#import <FLEX/FLEXRuntime+Compare.h>
#import <FLEX/FLEXRuntime+UIKitHelpers.h>
#import <FLEX/FLEXProtocolBuilder.h>
#import <FLEX/FLEXClassBuilder.h>
+17 -1
View File
@@ -3,7 +3,23 @@
// FLEX
//
// Created by Eric Horacek on 7/18/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
// Modified by Tanner Bennett on 3/12/20.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <FLEX/FLEXManager.h>
#import <FLEX/FLEXManager+Extensibility.h>
#import <FLEX/FLEXManager+Networking.h>
#import <FLEX/FLEXExplorerToolbar.h>
#import <FLEX/FLEXExplorerToolbarItem.h>
#import <FLEX/FLEXGlobalsEntry.h>
#import <FLEX/FLEX-Core.h>
#import <FLEX/FLEX-Runtime.h>
#import <FLEX/FLEX-Categories.h>
#import <FLEX/FLEX-ObjectExploring.h>
#import <FLEX/FLEXMacros.h>
#import <FLEX/FLEXAlert.h>
#import <FLEX/FLEXResources.h>
-99
View File
@@ -1,99 +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>
#if !FLEX_AT_LEAST_IOS13_SDK
@class UIWindowScene;
#endif
typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, readonly) BOOL isHidden;
- (void)showExplorer;
- (void)hideExplorer;
- (void)toggleExplorer;
/// Use this to present the explorer in a specific scene when the one
/// it chooses by default is not the one you wish to display it in.
- (void)showExplorerFromScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0));
#pragma mark - Network Debugging
/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
/// Full responses are kept temporarily in a size-limited cache and may be pruned under memory pressure.
@property (nonatomic, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Defaults to 25 MB if never set. Values set here are persisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
/// Useful to remove requests that are typically noisy, such as analytics requests that you aren't interested in tracking.
@property (nonatomic, copy) NSArray<NSString *> *networkRequestHostBlacklist;
#pragma mark - Keyboard Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
@property (nonatomic) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
/// @param modifiers Modifier keys such as shift, command, or alt/option
/// @param action The block to run on the main thread when the key & modifier combination is recognized.
/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
/// @note The action block will be retained for the duration of the application. You may want to use weak references.
/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
#pragma mark - Extensions
/// Default database password is @c nil by default.
/// Set this to the password you want the databases to open with.
@property (copy, nonatomic) NSString *defaultSqliteDatabasePassword;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
/// @note This method must be called from the main thread.
/// The objectFutureBlock will be invoked from the main thread and may return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
/// Sets custom viewer for specific content type.
/// @param contentType Mime type like application/json
/// @param viewControllerFutureBlock Viewer (view controller) creation block
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)setCustomViewerForContentType:(NSString *)contentType
viewControllerFutureBlock:(FLEXCustomContentViewerFuture)viewControllerFutureBlock;
@end
@@ -0,0 +1,19 @@
//
// FLEXDBQueryRowCell.h
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
extern NSString * const kFLEXDBQueryRowCellReuse;
@interface FLEXDBQueryRowCell : UITableViewCell
/// An array of NSString, NSNumber, or NSData objects
@property (nonatomic) NSArray *data;
@end
@@ -0,0 +1,74 @@
//
// FLEXDBQueryRowCell.m
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015 f. All rights reserved.
//
#import "FLEXDBQueryRowCell.h"
#import "FLEXMultiColumnTableView.h"
#import "NSArray+FLEX.h"
#import "UIFont+FLEX.h"
#import "FLEXColor.h"
NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
@interface FLEXDBQueryRowCell ()
@property (nonatomic) NSInteger columnCount;
@property (nonatomic) NSArray<UILabel *> *labels;
@end
@implementation FLEXDBQueryRowCell
- (void)setData:(NSArray *)data {
_data = data;
self.columnCount = data.count;
[self.labels flex_forEach:^(UILabel *label, NSUInteger idx) {
id content = self.data[idx];
if ([content isKindOfClass:[NSString class]]) {
label.text = content;
} else if (content == NSNull.null) {
label.text = @"<null>";
label.textColor = FLEXColor.deemphasizedTextColor;
} else {
label.text = [content description];
}
}];
}
- (void)setColumnCount:(NSInteger)columnCount {
if (columnCount != _columnCount) {
_columnCount = columnCount;
// Remove existing labels
for (UILabel *l in self.labels) {
[l removeFromSuperview];
}
// Create new labels
self.labels = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
UILabel *label = [UILabel new];
label.font = UIFont.flex_defaultTableCellFont;
label.textAlignment = NSTextAlignmentLeft;
[self.contentView addSubview:label];
return label;
}];
}
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.contentView.frame.size.width / self.labels.count;
CGFloat height = self.contentView.frame.size.height;
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
label.frame = CGRectMake(width * i + 5, 0, (width - 10), height);
}];
}
@end
@@ -12,15 +12,23 @@
// which Flying Meat Inc. licenses this file to you.
#import <Foundation/Foundation.h>
#import "FLEXSQLResult.h"
/// Conformers should automatically open and close the database
@protocol FLEXDatabaseManager <NSObject>
@required
- (instancetype)initWithPath:(NSString*)path;
- (BOOL)open;
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName;
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName;
/// @return \c nil if the database couldn't be opened
+ (instancetype)managerForDatabase:(NSString *)path;
/// @return a list of all table names
- (NSArray<NSString *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName;
@optional
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
@end
@@ -14,8 +14,8 @@
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
@required
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text;
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType;
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row;
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectHeaderForColumn:(NSInteger)column sortType:(FLEXTableColumnHeaderSortType)sortType;
@end
@@ -25,10 +25,9 @@
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSString *)columnNameInColumn:(NSInteger)column;
- (NSString *)rowNameInRow:(NSInteger)row;
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
- (NSArray *)contentAtRow:(NSInteger)row;
- (NSString *)columnTitle:(NSInteger)column;
- (NSString *)rowTitle:(NSInteger)row;
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
@@ -40,8 +39,8 @@
@interface FLEXMultiColumnTableView : UIView
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource>dataSource;
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate>delegate;
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource> dataSource;
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate> delegate;
- (void)reloadData;
@@ -7,11 +7,13 @@
//
#import "FLEXMultiColumnTableView.h"
#import "FLEXTableContentCell.h"
#import "FLEXDBQueryRowCell.h"
#import "FLEXTableLeftCell.h"
#import "FLEXColor.h"
@interface FLEXMultiColumnTableView ()
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
@interface FLEXMultiColumnTableView () <
UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate
>
@property (nonatomic) UIScrollView *contentScrollView;
@property (nonatomic) UIScrollView *headerScrollView;
@@ -19,38 +21,49 @@
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
/// \c NSNotFound if no column selected
@property (nonatomic) NSInteger sortColumn;
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
@property (nonatomic) NSArray *rowData;
@property (nonatomic, readonly) NSInteger numberOfColumns;
@property (nonatomic, readonly) NSInteger numberOfRows;
@property (nonatomic, readonly) CGFloat topHeaderHeight;
@property (nonatomic, readonly) CGFloat leftHeaderWidth;
@property (nonatomic, readonly) CGFloat columnMargin;
@end
static const CGFloat kColumnMargin = 1;
@implementation FLEXMultiColumnTableView
#pragma mark - Initialization
- (instancetype)initWithFrame:(CGRect)frame
{
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self loadUI];
self.autoresizingMask |= UIViewAutoresizingFlexibleWidth;
self.autoresizingMask |= UIViewAutoresizingFlexibleHeight;
self.autoresizingMask |= UIViewAutoresizingFlexibleTopMargin;
self.backgroundColor = FLEXColor.groupedBackgroundColor;
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
return self;
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self reloadData];
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
CGFloat topheaderHeight = [self topHeaderHeight];
CGFloat leftHeaderWidth = [self leftHeaderWidth];
CGFloat topheaderHeight = self.topHeaderHeight;
CGFloat leftHeaderWidth = self.leftHeaderWidth;
CGFloat topInsets = 0.f;
if (@available (iOS 11.0, *)) {
@@ -58,50 +71,45 @@ static const CGFloat kColumnMargin = 1;
}
CGFloat contentWidth = 0.0;
NSInteger rowsCount = [self numberOfColumns];
NSInteger rowsCount = self.numberOfColumns;
for (int i = 0; i < rowsCount; i++) {
contentWidth += [self contentWidthForColumn:i];
}
self.leftTableView.frame = CGRectMake(0, topheaderHeight + topInsets, leftHeaderWidth, height - topheaderHeight - topInsets);
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight);
self.headerScrollView.contentSize = CGSizeMake( self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height);
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight - topInsets);
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, height - topheaderHeight - topInsets);
CGFloat contentHeight = height - topheaderHeight - topInsets;
self.leftHeader.frame = CGRectMake(0, topInsets, self.leftHeaderWidth, self.topHeaderHeight);
self.leftTableView.frame = CGRectMake(
0, topheaderHeight + topInsets, leftHeaderWidth, contentHeight
);
self.headerScrollView.frame = CGRectMake(
leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight
);
self.headerScrollView.contentSize = CGSizeMake(
self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height
);
self.contentTableView.frame = CGRectMake(
0, 0, contentWidth + self.numberOfColumns * self.columnMargin , contentHeight
);
self.contentScrollView.frame = CGRectMake(
leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, contentHeight
);
self.contentScrollView.contentSize = self.contentTableView.frame.size;
self.leftHeader.frame = CGRectMake(0, topInsets, [self leftHeaderWidth], [self topHeaderHeight]);
}
- (void)loadUI
{
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
- (void)reloadData
{
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
#pragma mark - UI
- (void)loadHeaderScrollView
{
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
self.headerScrollView = headerScrollView;
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
- (void)loadHeaderScrollView {
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
headerScrollView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
self.headerScrollView = headerScrollView;
[self addSubview:headerScrollView];
}
- (void)loadContentScrollView
{
- (void)loadContentScrollView {
UIScrollView *scrollView = [UIScrollView new];
scrollView.bounces = NO;
scrollView.delegate = self;
@@ -110,149 +118,137 @@ static const CGFloat kColumnMargin = 1;
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[tableView registerClass:[FLEXDBQueryRowCell class]
forCellReuseIdentifier:kFLEXDBQueryRowCellReuse
];
[self addSubview:scrollView];
[scrollView addSubview:tableView];
[self addSubview:scrollView];
self.contentScrollView = scrollView;
self.contentTableView = tableView;
self.contentTableView = tableView;
}
- (void)loadLeftView
{
UITableView *leftTableView = [UITableView new];
- (void)loadLeftView {
UITableView *leftTableView = [UITableView new];
leftTableView.delegate = self;
leftTableView.dataSource = self;
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.leftTableView = leftTableView;
[self addSubview:leftTableView];
UIView *leftHeader = [UIView new];
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
UIView *leftHeader = [UIView new];
leftHeader.backgroundColor = FLEXColor.secondaryBackgroundColor;
self.leftHeader = leftHeader;
[self addSubview:leftHeader];
}
#pragma mark - Data
- (void)loadHeaderData
{
NSArray<UIView *> *subviews = self.headerScrollView.subviews;
for (UIView *subview in subviews) {
- (void)reloadData {
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
- (void)loadHeaderData {
// Remove existing headers, if any
for (UIView *subview in self.headerScrollView.subviews) {
[subview removeFromSuperview];
}
CGFloat x = 0.0;
CGFloat w = 0.0;
for (int i = 0; i < [self numberOfColumns] ; i++) {
w = [self contentWidthForColumn:i] + [self columnMargin];
CGFloat xOffset = 0.0;
for (NSInteger column = 0; column < self.numberOfColumns; column++) {
CGFloat width = [self contentWidthForColumn:column] + self.columnMargin;
FLEXTableColumnHeader *cell = [[FLEXTableColumnHeader alloc] initWithFrame:CGRectMake(x, 0, w, [self topHeaderHeight] - 1)];
cell.label.text = [self columnTitleForColumn:i];
[self.headerScrollView addSubview:cell];
FLEXTableColumnHeader *header = [[FLEXTableColumnHeader alloc]
initWithFrame:CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1)
];
header.titleLabel.text = [self columnTitle:column];
FLEXTableColumnHeaderSortType type = [self.sortStatusDict[[self columnTitleForColumn:i]] integerValue];
[cell changeSortStatusWithType:type];
if (column == self.sortColumn) {
header.sortType = self.sortType;
}
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(contentHeaderTap:)];
[cell addGestureRecognizer:gesture];
cell.userInteractionEnabled = YES;
// Header tap gesture
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(contentHeaderTap:)
];
[header addGestureRecognizer:gesture];
header.userInteractionEnabled = YES;
x = x + w;
[self.headerScrollView addSubview:header];
xOffset += width;
}
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture
{
FLEXTableColumnHeader *header = (FLEXTableColumnHeader *)gesture.view;
NSString *string = header.label.text;
FLEXTableColumnHeaderSortType currentType = [self.sortStatusDict[string] integerValue];
FLEXTableColumnHeaderSortType newType ;
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
NSInteger newSortColumn = [self.headerScrollView.subviews indexOfObject:gesture.view];
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
switch (currentType) {
case FLEXTableColumnHeaderSortTypeNone:
newType = FLEXTableColumnHeaderSortTypeAsc;
break;
case FLEXTableColumnHeaderSortTypeAsc:
newType = FLEXTableColumnHeaderSortTypeDesc;
break;
case FLEXTableColumnHeaderSortTypeDesc:
newType = FLEXTableColumnHeaderSortTypeAsc;
break;
}
// Reset old header
FLEXTableColumnHeader *oldHeader = (id)self.headerScrollView.subviews[self.sortColumn];
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
self.sortStatusDict = @{header.label.text : @(newType)};
[header changeSortStatusWithType:newType];
[self.delegate multiColumnTableView:self didTapHeaderWithText:string sortType:newType];
// Update new header
FLEXTableColumnHeader *newHeader = (id)self.headerScrollView.subviews[newSortColumn];
newHeader.sortType = newType;
// Update self
self.sortColumn = newSortColumn;
self.sortType = newType;
// Notify delegate
[self.delegate multiColumnTableView:self didSelectHeaderForColumn:newSortColumn sortType:newType];
}
- (void)loadContentData
{
- (void)loadContentData {
[self.contentTableView reloadData];
}
- (void)loadLeftViewData
{
- (void)loadLeftViewData {
[self.leftTableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIColor *backgroundColor = UIColor.whiteColor;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Alternating background color
UIColor *backgroundColor = FLEXColor.primaryBackgroundColor;
if (indexPath.row % 2 != 0) {
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
backgroundColor = FLEXColor.secondaryBackgroundColor;
}
if (tableView != self.leftTableView) {
self.rowData = [self.dataSource contentAtRow:indexPath.row];
FLEXTableContentCell *cell = [FLEXTableContentCell cellWithTableView:tableView
columnNumber:[self numberOfColumns]];
// Left side table view for row numbers
if (tableView == self.leftTableView) {
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
cell.contentView.backgroundColor = backgroundColor;
cell.delegate = self;
for (int i = 0 ; i < cell.labels.count; i++) {
UILabel *label = cell.labels[i];
label.textColor = UIColor.blackColor;
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
if ([content isEqualToString:@"<null>"]) {
label.textColor = UIColor.lightGrayColor;
content = @"NULL";
}
label.text = content;
label.backgroundColor = backgroundColor;
}
cell.titlelabel.text = [self rowTitle:indexPath.row];
return cell;
}
// Right side table view for data
else {
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
self.rowData = [self.dataSource contentForRow:indexPath.row];
FLEXDBQueryRowCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
];
cell.contentView.backgroundColor = backgroundColor;
cell.titlelabel.text = [self rowTitleForRow:indexPath.row];
cell.data = [self.dataSource contentForRow:indexPath.row];
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
return cell;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataSource numberOfRowsInTableView:self];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Scroll all scroll views in sync
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.contentScrollView) {
self.headerScrollView.contentOffset = scrollView.contentOffset;
}
@@ -267,80 +263,59 @@ static const CGFloat kColumnMargin = 1;
}
}
#pragma mark -
#pragma mark UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.leftTableView) {
[self.contentTableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
[self.contentTableView
selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone
];
}
else if (tableView == self.contentTableView) {
[self.leftTableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
[self.delegate multiColumnTableView:self didSelectRow:indexPath.row];
}
}
#pragma mark -
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows
{
- (NSInteger)numberOfRows {
return [self.dataSource numberOfRowsInTableView:self];
}
- (NSInteger)numberOfColumns
{
- (NSInteger)numberOfColumns {
return [self.dataSource numberOfColumnsInTableView:self];
}
- (NSString *)columnTitleForColumn:(NSInteger)column
{
return [self.dataSource columnNameInColumn:column];
- (NSString *)columnTitle:(NSInteger)column {
return [self.dataSource columnTitle:column];
}
- (NSString *)rowTitleForRow:(NSInteger)row
{
return [self.dataSource rowNameInRow:row];
- (NSString *)rowTitle:(NSInteger)row {
return [self.dataSource rowTitle:row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
{
return [self.dataSource contentAtColumn:column row:row];
}
- (CGFloat)contentWidthForColumn:(NSInteger)column
{
- (CGFloat)contentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row
{
- (CGFloat)contentHeightForRow:(NSInteger)row {
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
}
- (CGFloat)topHeaderHeight
{
- (CGFloat)topHeaderHeight {
return [self.dataSource heightForTopHeaderInTableView:self];
}
- (CGFloat)leftHeaderWidth
{
- (CGFloat)leftHeaderWidth {
return [self.dataSource widthForLeftHeaderInTableView:self];
}
- (CGFloat)columnMargin
{
- (CGFloat)columnMargin {
return kColumnMargin;
}
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text
{
[self.delegate multiColumnTableView:self didTapLabelWithText:text];
}
@end
@@ -7,6 +7,8 @@
//
#import "FLEXRealmDatabaseManager.h"
#import "NSArray+FLEX.h"
#import "FLEXSQLResult.h"
#if __has_include(<Realm/Realm.h>)
#import <Realm/Realm.h>
@@ -18,97 +20,81 @@
@interface FLEXRealmDatabaseManager ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic) RLMRealm * realm;
@property (nonatomic) RLMRealm *realm;
@end
//#endif
@implementation FLEXRealmDatabaseManager
static Class RLMRealmClass = nil;
- (instancetype)initWithPath:(NSString*)aPath
{
Class realmClass = NSClassFromString(@"RLMRealm");
if (realmClass == nil) {
+ (void)load {
RLMRealmClass = NSClassFromString(@"RLMRealm");
}
+ (instancetype)managerForDatabase:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (instancetype)initWithPath:(NSString *)path {
if (!RLMRealmClass) {
return nil;
}
self = [super init];
if (self) {
_path = aPath;
_path = path;
if (![self open]) {
return nil;
}
}
return self;
}
- (BOOL)open
{
Class realmClass = NSClassFromString(@"RLMRealm");
- (BOOL)open {
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
if (realmClass == nil || configurationClass == nil) {
if (!RLMRealmClass || !configurationClass) {
return NO;
}
NSError *error = nil;
id configuration = [configurationClass new];
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
self.realm = [RLMRealmClass realmWithConfiguration:configuration error:&error];
return (error == nil);
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables
{
NSMutableArray<NSDictionary<NSString *, id> *> *allTables = [NSMutableArray array];
RLMSchema *schema = [self.realm schema];
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
if (objectSchema.className == nil) {
continue;
}
NSDictionary<NSString *, id> *dictionary = @{@"name":objectSchema.className};
[allTables addObject:dictionary];
}
return allTables;
- (NSArray<NSString *> *)queryAllTables {
// Map each schema to its name
return [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) {
return schema.className ?: nil;
}];
}
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName
{
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
if (objectSchema == nil) {
return nil;
}
NSMutableArray<NSString *> *columnNames = [NSMutableArray array];
for (RLMProperty *property in objectSchema.properties) {
[columnNames addObject:property.name];
}
return columnNames;
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
// Map each column to its name
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
return property.name;
}];
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName
{
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
RLMResults *results = [self.realm allObjects:tableName];
if (results.count == 0 || objectSchema == nil) {
if (results.count == 0 || !objectSchema) {
return nil;
}
NSMutableArray<NSDictionary<NSString *, id> *> *allDataEntries = [NSMutableArray array];
for (RLMObject *result in results) {
NSMutableDictionary<NSString *, id> *entry = [NSMutableDictionary dictionary];
for (RLMProperty *property in objectSchema.properties) {
id value = [result valueForKey:property.name];
entry[property.name] = (value) ? (value) : [NSNull null];
}
[allDataEntries addObject:entry];
}
return allDataEntries;
// Map results to an array of rows
return [NSArray flex_mapped:results block:^id(RLMObject *result, NSUInteger idx) {
// Map each row to an array of the values of its properties
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
return [result valueForKey:property.name] ?: NSNull.null;
}];
}];
}
@end
@@ -0,0 +1,48 @@
//
// FLEXSQLResult.h
// FLEX
//
// Created by Tanner on 3/3/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXSQLResult : NSObject
/// Describes the result of a non-select query, or an error of any kind of query
+ (instancetype)message:(NSString *)message;
/// Describes the result of a known failed execution
+ (instancetype)error:(NSString *)message;
/// @param rowData A list of rows, where each element in the row
/// corresponds to the column given in /c columnNames
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;
@property (nonatomic, readonly, nullable) NSString *message;
/// A value of YES means this is surely an error,
/// but it still might be an error even with a value of NO
@property (nonatomic, readonly) BOOL isError;
/// A list of column names
@property (nonatomic, readonly, nullable) NSArray<NSString *> *columns;
/// A list of rows, where each element in the row corresponds
/// to the value of the column at the same index in \c columns.
///
/// That is, given a row, looping over the contents of the row and
/// the contents of \c columns will give you key-value pairs of
/// column names to column values for that row.
@property (nonatomic, readonly, nullable) NSArray<NSArray<NSString *> *> *rows;
/// A list of rows where the fields are paired to column names.
///
/// This property is lazily constructed by looping over
/// the rows and columns present in the other two properties.
@property (nonatomic, readonly, nullable) NSArray<NSDictionary<NSString *, id> *> *keyedRows;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,53 @@
//
// FLEXSQLResult.m
// FLEX
//
// Created by Tanner on 3/3/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXSQLResult.h"
#import "NSArray+FLEX.h"
@implementation FLEXSQLResult
@synthesize keyedRows = _keyedRows;
+ (instancetype)message:(NSString *)message {
return [[self alloc] initWithmessage:message columns:nil rows:nil];
}
+ (instancetype)error:(NSString *)message {
FLEXSQLResult *result = [self message:message];
result->_isError = YES;
return result;
}
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
}
- (id)initWithmessage:(NSString *)message columns:(NSArray *)columns rows:(NSArray<NSArray *> *)rows {
NSParameterAssert(message || (columns && rows));
NSParameterAssert(columns.count == rows.firstObject.count);
self = [super init];
if (self) {
_message = message;
_columns = columns;
_rows = rows;
}
return self;
}
- (NSArray<NSDictionary<NSString *,id> *> *)keyedRows {
if (!_keyedRows) {
_keyedRows = [self.rows flex_mapped:^id(NSArray<NSString *> *row, NSUInteger idx) {
return [NSDictionary dictionaryWithObjects:row forKeys:self.columns];
}];
}
return _keyedRows;
}
@end
@@ -13,7 +13,20 @@
#import <Foundation/Foundation.h>
#import "FLEXDatabaseManager.h"
#import "FLEXSQLResult.h"
@interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
/// Contains the result of the last operation, which may be an error
@property (nonatomic, readonly) FLEXSQLResult *lastResult;
/// Calls into \c sqlite3_last_insert_rowid()
@property (nonatomic, readonly) NSInteger lastRowID;
/// Given a statement like 'SELECT * from @table where @col = @val' and arguments
/// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will
/// invoke the statement and properly bind the given arguments to the statement.
///
/// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values.
- (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args;
@end
@@ -8,62 +8,71 @@
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXManager.h"
#import "NSArray+FLEX.h"
#import "FLEXRuntimeConstants.h"
#import <sqlite3.h>
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
@interface FLEXSQLiteDatabaseManager ()
@property (nonatomic) sqlite3 *db;
@property (nonatomic, copy) NSString *path;
@end
@implementation FLEXSQLiteDatabaseManager
{
sqlite3* _db;
NSString* _databasePath;
#pragma mark - FLEXDatabaseManager
+ (instancetype)managerForDatabase:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (instancetype)initWithPath:(NSString*)aPath
{
- (instancetype)initWithPath:(NSString *)path {
self = [super init];
if (self) {
_databasePath = [aPath copy];
self.path = path;;
}
return self;
}
- (void)dealloc {
[self close];
}
- (BOOL)open {
if (_db) {
if (self.db) {
return YES;
}
int err = sqlite3_open(_databasePath.UTF8String, &_db);
int err = sqlite3_open(self.path.UTF8String, &_db);
#if SQLITE_HAS_CODEC
NSString *defaultSqliteDatabasePassword = [FLEXManager sharedManager].defaultSqliteDatabasePassword;
NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword;
if (defaultSqliteDatabasePassword) {
const char *key = defaultSqliteDatabasePassword.UTF8String;
sqlite3_key(_db, key, (int)strlen(key));
}
#endif
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
if (err != SQLITE_OK) {
return [self storeErrorForLastTask:@"Open"];
}
return YES;
}
- (BOOL)close {
if (!_db) {
if (!self.db) {
return YES;
}
int rc;
BOOL retry;
BOOL triedFinalizingOpenStatements = NO;
BOOL retry, triedFinalizingOpenStatements = NO;
do {
retry = NO;
rc = sqlite3_close(_db);
retry = NO;
rc = sqlite3_close(_db);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
@@ -74,130 +83,223 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
retry = YES;
}
}
} else if (SQLITE_OK != rc) {
[self storeErrorForLastTask:@"Close"];
self.db = nil;
return NO;
}
else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
}
}
while (retry);
} while (retry);
_db = nil;
self.db = nil;
return YES;
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables
{
return [self executeQuery:QUERY_TABLENAMES_SQL];
- (NSInteger)lastRowID {
return (NSInteger)sqlite3_last_insert_rowid(self.db);
}
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName
{
- (NSArray<NSString *> *)queryAllTables {
return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
return table.firstObject;
}];
}
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
NSArray<NSDictionary<NSString *, id> *> *resultArray = [self executeQuery:sql];
NSMutableArray<NSString *> *array = [NSMutableArray array];
for (NSDictionary<NSString *, id> *dict in resultArray) {
NSString *columnName = (NSString *)dict[@"name"] ?: @"";
[array addObject:columnName];
}
return array;
FLEXSQLResult *results = [self executeStatement:sql];
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}];
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName
{
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
return [self executeQuery:sql];
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
return [self executeStatement:[@"SELECT * FROM "
stringByAppendingString:tableName
]].rows;
}
#pragma mark -
#pragma mark - Private
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
return [self executeStatement:sql arguments:nil];
}
- (NSArray<NSDictionary<NSString *, id> *> *)executeQuery:(NSString *)sql
{
- (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args {
[self open];
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
FLEXSQLResult *result = nil;
sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
if (num_cols > 0) {
NSMutableDictionary<NSString *, id> *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count(pstmt);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(pstmt, columnIdx)];
id objectValue = [self objectForColumnIndex:columnIdx stmt:pstmt];
[dict setObject:objectValue forKey:columnName];
}
[resultArray addObject:dict];
int status;
if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) {
NSMutableArray<NSArray *> *rows = [NSMutableArray new];
// Bind parameters, if any
if (![self bindParameters:args toStatement:pstmt]) {
return self.lastResult;
}
// Grab columns
int columnCount = sqlite3_column_count(pstmt);
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return @(sqlite3_column_name(pstmt, (int)i));
}];
// Execute statement
while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) {
// Grab rows if this is a selection query
int dataCount = sqlite3_data_count(pstmt);
if (dataCount > 0) {
[rows addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return [self objectForColumnIndex:(int)i stmt:pstmt];
}]];
}
}
if (status == SQLITE_DONE) {
if (rows.count) {
// We selected some rows
result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
} else {
// We executed a query like INSERT, UDPATE, or DELETE
int rowsAffected = sqlite3_changes(_db);
NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected];
result = _lastResult = [FLEXSQLResult message:message];
}
} else {
// An error occured executing the query
result = _lastResult = [self errorResult:@"Execution"];
}
} else {
// An error occurred creating the prepared statement
result = _lastResult = [self errorResult:@"Prepared statement"];
}
[self close];
return resultArray;
sqlite3_finalize(pstmt);
return result;
}
#pragma mark - Private
/// @return YES on success, NO if an error was encountered and stored in \c lastResult
- (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt {
for (NSString *param in args.allKeys) {
int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String);
id value = args[param];
if (idx == 0) {
// No parameter matching that arg
@throw NSInternalInconsistencyException;
}
// Null
if ([value isKindOfClass:[NSNull class]]) {
status = sqlite3_bind_null(pstmt, idx);
}
// String params
else if ([value isKindOfClass:[NSString class]]) {
const char *str = [value UTF8String];
status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT);
}
// Data params
else if ([value isKindOfClass:[NSData class]]) {
const void *blob = [value bytes];
status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT);
}
// Primitive params
else if ([value isKindOfClass:[NSNumber class]]) {
FLEXTypeEncoding type = [value objCType][0];
switch (type) {
case FLEXTypeEncodingCBool:
case FLEXTypeEncodingChar:
case FLEXTypeEncodingUnsignedChar:
case FLEXTypeEncodingShort:
case FLEXTypeEncodingUnsignedShort:
case FLEXTypeEncodingInt:
case FLEXTypeEncodingUnsignedInt:
case FLEXTypeEncodingLong:
case FLEXTypeEncodingUnsignedLong:
case FLEXTypeEncodingLongLong:
case FLEXTypeEncodingUnsignedLongLong:
status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]);
break;
case FLEXTypeEncodingFloat:
case FLEXTypeEncodingDouble:
status = sqlite3_bind_double(pstmt, idx, [value doubleValue]);
break;
default:
@throw NSInternalInconsistencyException;
break;
}
}
// Unsupported type
else {
@throw NSInternalInconsistencyException;
}
if (status != SQLITE_OK) {
return [self storeErrorForLastTask:
[NSString stringWithFormat:@"Binding param named '%@'", param]
];
}
}
return YES;
}
- (BOOL)storeErrorForLastTask:(NSString *)action {
_lastResult = [self errorResult:action];
return NO;
}
- (FLEXSQLResult *)errorResult:(NSString *)description {
const char *error = sqlite3_errmsg(_db);
NSString *message = error ? @(error) : [NSString
stringWithFormat:@"(%@: empty error", description
];
return [FLEXSQLResult error:message];
}
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
int columnType = sqlite3_column_type(stmt, columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:sqlite3_column_int64(stmt, columnIdx)];
switch (columnType) {
case SQLITE_INTEGER:
return @(sqlite3_column_int64(stmt, columnIdx)).stringValue;
case SQLITE_FLOAT:
return @(sqlite3_column_double(stmt, columnIdx)).stringValue;
case SQLITE_BLOB:
return [NSString stringWithFormat:@"Data (%@ bytes)",
@([self dataForColumnIndex:columnIdx stmt:stmt].length)
];
default:
// Default to a string for everything else
return [self stringForColumnIndex:columnIdx stmt:stmt] ?: NSNull.null;
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:sqlite3_column_double(stmt, columnIdx)];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [self dataForColumnIndex:columnIdx stmt:stmt];
}
else {
//default to a string for everything else
returnValue = [self stringForColumnIndex:columnIdx stmt:stmt];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || columnIdx < 0) {
return nil;
}
const char *text = (const char *)sqlite3_column_text(stmt, columnIdx);
return text ? @(text) : nil;
}
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *c = (const char *)sqlite3_column_text(stmt, columnIdx);
const void *blob = sqlite3_column_blob(stmt, columnIdx);
NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx);
if (!c) {
// null row.
return nil;
}
return [NSString stringWithUTF8String:c];
return blob ? [NSData dataWithBytes:blob length:size] : nil;
}
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt{
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob(stmt, columnIdx);
int dataSize = sqlite3_column_bytes(stmt, columnIdx);
if (dataBuffer == NULL) {
return nil;
}
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
}
@end

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