Compare commits

...

141 Commits

Author SHA1 Message Date
Tanner Bennett 8e86ffccd6 Bump podspec version, add warning flag to podspec
Our podspec needs -Wno-unsupported-availability-guard for now
2019-12-09 15:34:20 -06:00
Tanner Bennett 228de102e7 Fix #359
Custom UIMenu callouts were not not appearing because the UITextEffectsWindow has a lower level than the FLEX window by default. Until a text field is activated, it would sit below the FLEX window, and custom callouts (copy, copy address) would not appear.
2019-12-09 14:54:41 -06:00
Tanner Bennett 37b5d1be2a Swipe to delete keychain items 2019-12-09 10:48:28 -06:00
renwei.chen ef8f866330 Bug fixes
- Fix root view controller global not working
- Fix crash in keychain viewer when viewing NSData
2019-12-09 10:48:28 -06:00
Tanner Bennett e862b81734 Update README.md 2019-12-05 17:26:10 -06:00
Tanner Bennett a3f66b3f87 Fix scope carousel not working properly on iOS 13 2019-12-02 23:13:38 -06:00
Tanner Bennett 3e12ad9887 Add explicit header titles to several screens
Some screens used the ugly "plain" table view or used a group table view with no title. Both look awful. This commit updates the cookies, keychain, libraries, and live objects screens to use grouped table views with a descriptive section header, like "5 cookies" or "123 of 456 objects, 30 MB"

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

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

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

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

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

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

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

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

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

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

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

* [DarkMode] Load icons as templates

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

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

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

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

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

* [DarkMode] Use dynamic color providers for iOS13+

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

* [DarkMode] Remove last reference for scrollViewGrayColor

* [DarkMode] Rename backgroundColor* to systemBackgroundColor*

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

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

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

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

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

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

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

It may be worth considering other approaches to this entirely, such as an alert that asks you if you want to get or set the ivar/property before a new screen is even pushed, or maybe a "Get" button as an accessory view on the rows of mutable ivars/properties.
2018-11-23 00:00:30 -06:00
261 changed files with 9865 additions and 4215 deletions
+15
View File
@@ -0,0 +1,15 @@
//
// FLEXCarouselCell.h
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXCarouselCell : UICollectionViewCell
@property (nonatomic, copy) NSString *title;
@end
+91
View File
@@ -0,0 +1,91 @@
//
// FLEXCarouselCell.m
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "UIView+FLEX_Layout.h"
@interface FLEXCarouselCell ()
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic, readonly) UIView *selectionIndicatorStripe;
@property (nonatomic) BOOL constraintsInstalled;
@end
@implementation FLEXCarouselCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_titleLabel = [UILabel new];
_selectionIndicatorStripe = [UIView new];
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.titleLabel.adjustsFontForContentSizeCategory = YES;
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.selectionIndicatorStripe];
[self installConstraints];
[self updateAppearance];
}
return self;
}
- (void)updateAppearance {
self.selectionIndicatorStripe.hidden = !self.selected;
if (self.selected) {
self.titleLabel.textColor = self.tintColor;
} else {
self.titleLabel.textColor = [FLEXColor deemphasizedTextColor];
}
}
#pragma mark Public
- (NSString *)title {
return self.titleLabel.text;
}
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
[self.titleLabel sizeToFit];
[self setNeedsLayout];
}
#pragma mark Overrides
- (void)prepareForReuse {
[super prepareForReuse];
[self updateAppearance];
}
- (void)installConstraints {
CGFloat stripeHeight = 2;
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
UIView *superview = self.contentView;
[self.titleLabel pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
[self.selectionIndicatorStripe.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = YES;
[self.selectionIndicatorStripe.heightAnchor constraintEqualToConstant:stripeHeight].active = YES;
}
- (void)setSelected:(BOOL)selected {
super.selected = selected;
[self updateAppearance];
}
@end
+20
View File
@@ -0,0 +1,20 @@
//
// FLEXScopeCarousel.h
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
/// Only use on iOS 10 and up. Requires iOS 10 APIs for calculating row sizes.
@interface FLEXScopeCarousel : UIControl
@property (nonatomic, copy) NSArray<NSString *> *items;
@property (nonatomic) NSInteger selectedIndex;
@property (nonatomic) void(^selectedIndexChangedAction)(NSInteger idx);
- (void)registerBlockForDynamicTypeChanges:(void(^)(FLEXScopeCarousel *))handler;
@end
+207
View File
@@ -0,0 +1,207 @@
//
// FLEXScopeCarousel.m
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXScopeCarousel.h"
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "UIView+FLEX_Layout.h"
const CGFloat kCarouselItemSpacing = 0;
NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, readonly) UICollectionView *collectionView;
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
@property (nonatomic, readonly) NSLayoutConstraint *heightConstraint;
@property (nonatomic, readonly) id dynamicTypeObserver;
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
@property (nonatomic) BOOL constraintsInstalled;
@end
@implementation FLEXScopeCarousel
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [FLEXColor primaryBackgroundColor];
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_dynamicTypeHandlers = [NSMutableArray new];
CGSize itemSize = CGSizeZero;
if (@available(iOS 10.0, *)) {
itemSize = UICollectionViewFlowLayoutAutomaticSize;
}
// Collection view layout
UICollectionViewFlowLayout *layout = ({
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.sectionInset = UIEdgeInsetsZero;
layout.minimumLineSpacing = kCarouselItemSpacing;
layout.itemSize = itemSize;
layout.estimatedItemSize = itemSize;
layout;
});
// Collection view
_collectionView = ({
UICollectionView *cv = [[UICollectionView alloc]
initWithFrame:CGRectZero
collectionViewLayout:layout
];
cv.showsHorizontalScrollIndicator = NO;
cv.backgroundColor = UIColor.clearColor;
cv.delegate = self;
cv.dataSource = self;
[cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
[self addSubview:cv];
cv;
});
// Sizing cell
_sizingCell = [FLEXCarouselCell new];
self.sizingCell.title = @"NSObject";
// Dynamic type
__weak __typeof(self) weakSelf = self;
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
addObserverForName:UIContentSizeCategoryDidChangeNotification
object:nil queue:nil usingBlock:^(NSNotification *note) {
[self.collectionView setNeedsLayout];
[self setNeedsUpdateConstraints];
// Notify observers
__typeof(self) self = weakSelf;
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
block(self);
}
}
];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver];
}
#pragma mark - Overrides
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGFloat width = 1.f / UIScreen.mainScreen.scale;
// Draw hairline
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [FLEXColor hairlineColor].CGColor);
CGContextSetLineWidth(context, width);
CGContextMoveToPoint(context, 0, rect.size.height - width);
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
CGContextStrokePath(context);
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor].active = YES;
[self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor].active = YES;
[self.topAnchor constraintEqualToAnchor:self.superview.topAnchor].active = YES;
[self.collectionView pinEdgesToSuperview];
_heightConstraint = [self.heightAnchor constraintEqualToConstant:100];
self.heightConstraint.active = YES;
self.constraintsInstalled = YES;
}
self.heightConstraint.constant = [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[super updateConstraints];
}
#pragma mark - Public
- (void)setItems:(NSArray<NSString *> *)items {
NSParameterAssert(items.count);
_items = items.copy;
// Refresh list, select first item initially
[self.collectionView reloadData];
self.selectedIndex = 0;
}
- (void)setSelectedIndex:(NSInteger)idx {
NSParameterAssert(idx < self.items.count);
_selectedIndex = idx;
NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
[self.collectionView selectItemAtIndexPath:path
animated:YES
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
}
- (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
[self.dynamicTypeHandlers addObject:handler];
}
#pragma mark - UICollectionView
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
// if (@available(iOS 10.0, *)) {
// return UICollectionViewFlowLayoutAutomaticSize;
// }
self.sizingCell.title = self.items[indexPath.item];
return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.items.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
forIndexPath:indexPath];
cell.title = self.items[indexPath.row];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
_selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
if (self.selectedIndexChangedAction) {
self.selectedIndexChangedAction(indexPath.row);
}
// TODO: dynamically choose a scroll position. Very wide items should
// get "Left" while smaller items should not scroll at all, unless
// they are only partially on the screen, in which case they
// should get "HorizontallyCentered" to bring them onto the screen.
// For now, everything goes to the left, as this has a similar effect.
[collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionLeft
animated:YES];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
@end
+96
View File
@@ -0,0 +1,96 @@
//
// FLEXTableViewController.h
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXScopeCarousel;
typedef CGFloat FLEXDebounceInterval;
/// No delay, all events delivered
extern CGFloat const kFLEXDebounceInstant;
/// Small delay which makes UI seem smoother by avoiding rapid events
extern CGFloat const kFLEXDebounceFast;
/// Slower than Fast, faster than ExpensiveIO
extern CGFloat const kFLEXDebounceForAsyncSearch;
/// The least frequent, at just over once per second; for I/O or other expensive operations
extern CGFloat const kFLEXDebounceForExpensiveIO;
@interface FLEXTableViewController : UITableViewController <UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate>
/// A grouped table view. Inset on iOS 13.
///
/// Simply calls into initWithStyle:
- (id)init;
/// Defaults to NO.
///
/// Setting this to YES will initialize the carousel and the view.
@property (nonatomic) BOOL showsCarousel;
/// A horizontally scrolling list with functionality similar to
/// that of a search bar's scope bar. You'd want to use this when
/// you have potentially more than 4 scope options.
@property (nonatomic) FLEXScopeCarousel *carousel;
/// Defaults to NO.
///
/// Setting this to YES will initialize searchController and the view.
@property (nonatomic) BOOL showsSearchBar;
/// Defaults to NO.
///
/// Setting this to YES will make the search bar appear whenever the view appears.
/// Otherwise, iOS will only show the search bar when you scroll up.
@property (nonatomic) BOOL showSearchBarInitially;
/// nil unless showsSearchBar is set to YES.
///
/// self is used as the default search results updater and delegate.
/// Make sure your subclass conforms to UISearchControllerDelegate.
/// The search bar will not dim the background or hide the navigation bar by default.
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
@property (nonatomic) UISearchController *searchController;
/// Used to initialize the search controller. Defaults to nil.
@property (nonatomic) UIViewController *searchResultsController;
/// Defaults to "Fast"
///
/// Determines how often search bar results will be "debounced."
/// Empty query events are always sent instantly. Query events will
/// be sent when the user has not changed the query for this interval.
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
/// Whether the search bar stays at the top of the view while scrolling.
///
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
/// or it will not be respsected. Use this instead.
/// Defaults to NO.
@property (nonatomic) BOOL pinSearchBar;
/// By default, we will show the search bar's cancel button when
/// search becomes active and hide it when search is dismissed.
///
/// Do not set the showsCancelButton property on the searchController's
/// searchBar manually. Set this property after turning on showsSearchBar.
///
/// Does nothing pre-iOS 13, safe to call on any version.
@property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton;
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
@property (nonatomic, readonly) NSInteger selectedScope;
/// self.searchController.searchBar.text
@property (nonatomic, readonly) NSString *searchText;
/// Subclasses should override to handle search query update events.
///
/// searchBarDebounceInterval is used to reduce the frequency at which this method is called.
/// This method is also called when the search bar becomes the first responder,
/// and when the selected search bar scope index changes.
- (void)updateSearchResults:(NSString *)newText;
/// Convenient for doing some async processor-intensive searching
/// in the background before updating the UI back on the main queue.
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
@end
+293
View File
@@ -0,0 +1,293 @@
//
// 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
+39
View File
@@ -0,0 +1,39 @@
//
// FLEXTableViewSection.h
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// A protocol for arbitrary case-insensitive pattern matching
@protocol FLEXPatternMatching <NSObject>
/// @return YES if the receiver matches the query, case-insensitive
- (BOOL)matches:(NSString *)query;
@end
@interface FLEXTableViewSection<__covariant ObjectType> : NSObject
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray<ObjectType<FLEXPatternMatching>> *)rows;
@property (nonatomic, readonly) NSInteger section;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSArray<ObjectType<FLEXPatternMatching>> *rows;
@property (nonatomic, readonly) NSInteger count;
/// @return A new section containing only rows that match the string,
/// or nil if the section was empty and no rows matched the string.
- (nullable instancetype)newSectionWithRowsMatchingQuery:(NSString *)query;
@end
@interface FLEXTableViewSection<__covariant ObjectType> (Subscripting)
- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
@end
NS_ASSUME_NONNULL_END
+49
View File
@@ -0,0 +1,49 @@
//
// FLEXTableViewSection.m
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
@implementation FLEXTableViewSection
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray *)rows {
FLEXTableViewSection *s = [self new];
s->_section = section;
s->_title = title;
s->_rows = rows.copy;
return s;
}
- (instancetype)newSectionWithRowsMatchingQuery:(NSString *)query {
// Find rows containing the search string
NSPredicate *containsString = [NSPredicate predicateWithBlock:^BOOL(id<FLEXPatternMatching> obj, NSDictionary *bindings) {
return [obj matches:query];
}];
NSArray *filteredRows = [self.rows filteredArrayUsingPredicate:containsString];
// Only return new section if not empty
if (filteredRows.count) {
return [[self class] section:self.section title:self.title rows:filteredRows];
}
return nil;
}
- (NSInteger)count {
return self.rows.count;
}
@end
@implementation FLEXTableViewSection (Subscripting)
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
return self.rows[idx];
}
@end
@@ -14,8 +14,8 @@
@interface FLEXColorComponentInputView : UIView
@property (nonatomic, strong) UISlider *slider;
@property (nonatomic, strong) UILabel *valueLabel;
@property (nonatomic) UISlider *slider;
@property (nonatomic) UILabel *valueLabel;
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
@@ -34,12 +34,12 @@
{
self = [super initWithFrame:frame];
if (self) {
self.slider = [[UISlider alloc] init];
self.slider = [UISlider new];
self.slider.backgroundColor = self.backgroundColor;
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.slider];
self.valueLabel = [[UILabel alloc] init];
self.valueLabel = [UILabel new];
self.valueLabel.backgroundColor = self.backgroundColor;
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
self.valueLabel.textAlignment = NSTextAlignmentRight;
@@ -94,9 +94,9 @@
@interface FLEXColorPreviewBox : UIView
@property (nonatomic, strong) UIColor *color;
@property (nonatomic) UIColor *color;
@property (nonatomic, strong) UIView *colorOverlayView;
@property (nonatomic) UIView *colorOverlayView;
@end
@@ -107,12 +107,12 @@
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1.0;
self.layer.borderColor = [[UIColor blackColor] CGColor];
self.layer.borderColor = UIColor.blackColor.CGColor;
self.backgroundColor = [UIColor colorWithPatternImage:[[self class] backgroundPatternImage]];
self.colorOverlayView = [[UIView alloc] initWithFrame:self.bounds];
self.colorOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.colorOverlayView.backgroundColor = [UIColor clearColor];
self.colorOverlayView.backgroundColor = UIColor.clearColor;
[self addSubview:self.colorOverlayView];
}
return self;
@@ -134,12 +134,12 @@
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [[UIScreen mainScreen] scale]);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale);
[[UIColor whiteColor] setFill];
[UIColor.whiteColor setFill];
UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));
[[UIColor grayColor] setFill];
[UIColor.grayColor setFill];
UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height));
UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height));
@@ -153,12 +153,12 @@
@interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate>
@property (nonatomic, strong) FLEXColorPreviewBox *colorPreviewBox;
@property (nonatomic, strong) UILabel *hexLabel;
@property (nonatomic, strong) FLEXColorComponentInputView *alphaInput;
@property (nonatomic, strong) FLEXColorComponentInputView *redInput;
@property (nonatomic, strong) FLEXColorComponentInputView *greenInput;
@property (nonatomic, strong) FLEXColorComponentInputView *blueInput;
@property (nonatomic) FLEXColorPreviewBox *colorPreviewBox;
@property (nonatomic) UILabel *hexLabel;
@property (nonatomic) FLEXColorComponentInputView *alphaInput;
@property (nonatomic) FLEXColorComponentInputView *redInput;
@property (nonatomic) FLEXColorComponentInputView *greenInput;
@property (nonatomic) FLEXColorComponentInputView *blueInput;
@end
@@ -168,32 +168,32 @@
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.colorPreviewBox = [[FLEXColorPreviewBox alloc] init];
self.colorPreviewBox = [FLEXColorPreviewBox new];
[self addSubview:self.colorPreviewBox];
self.hexLabel = [[UILabel alloc] init];
self.hexLabel = [UILabel new];
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
self.hexLabel.textAlignment = NSTextAlignmentCenter;
self.hexLabel.font = [FLEXUtility defaultFontOfSize:12.0];
[self addSubview:self.hexLabel];
self.alphaInput = [[FLEXColorComponentInputView alloc] init];
self.alphaInput.slider.minimumTrackTintColor = [UIColor blackColor];
self.alphaInput = [FLEXColorComponentInputView new];
self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor;
self.alphaInput.delegate = self;
[self addSubview:self.alphaInput];
self.redInput = [[FLEXColorComponentInputView alloc] init];
self.redInput.slider.minimumTrackTintColor = [UIColor redColor];
self.redInput = [FLEXColorComponentInputView new];
self.redInput.slider.minimumTrackTintColor = UIColor.redColor;
self.redInput.delegate = self;
[self addSubview:self.redInput];
self.greenInput = [[FLEXColorComponentInputView alloc] init];
self.greenInput.slider.minimumTrackTintColor = [UIColor greenColor];
self.greenInput = [FLEXColorComponentInputView new];
self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor;
self.greenInput.delegate = self;
[self addSubview:self.greenInput];
self.blueInput = [[FLEXColorComponentInputView alloc] init];
self.blueInput.slider.minimumTrackTintColor = [UIColor blueColor];
self.blueInput = [FLEXColorComponentInputView new];
self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor;
self.blueInput.delegate = self;
[self addSubview:self.blueInput];
}
@@ -221,8 +221,8 @@
[self.hexLabel sizeToFit];
const CGFloat kLabelVerticalOutsetAmount = 0.0;
const CGFloat kLabelHorizonalOutsetAmount = 2.0;
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount);
const CGFloat kLabelHorizontalOutsetAmount = 2.0;
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount);
self.hexLabel.frame = UIEdgeInsetsInsetRect(self.hexLabel.frame, labelOutset);
CGFloat hexLabelOriginX = self.colorPreviewBox.layer.borderWidth;
CGFloat hexLabelOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) - self.colorPreviewBox.layer.borderWidth - self.hexLabel.frame.size.height;
@@ -11,7 +11,7 @@
@interface FLEXArgumentInputDateView ()
@property (nonatomic, strong) UIDatePicker *datePicker;
@property (nonatomic) UIDatePicker *datePicker;
@end
@@ -21,7 +21,7 @@
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.datePicker = [[UIDatePicker alloc] init];
self.datePicker = [UIDatePicker new];
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
// Using UTC, because that's what the NSDate description prints
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
@@ -13,8 +13,8 @@
@interface FLEXArgumentInputFontView ()
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
@end
@@ -1,6 +1,6 @@
//
// FLEXArgumentInputFontsPickerView.h
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
@@ -1,6 +1,6 @@
//
// FLEXArgumentInputFontsPickerView.m
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
@@ -11,7 +11,7 @@
@interface FLEXArgumentInputFontsPickerView ()
@property (nonatomic, strong) NSMutableArray<NSString *> *availableFonts;
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
@end
@@ -40,7 +40,7 @@
- (id)inputValue
{
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
#pragma mark - private
@@ -74,7 +74,7 @@
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [self.availableFonts count];
return self.availableFonts.count;
}
#pragma mark - UIPickerViewDelegate
@@ -84,7 +84,7 @@
UILabel *fontLabel;
if (!view) {
fontLabel = [UILabel new];
fontLabel.backgroundColor = [UIColor clearColor];
fontLabel.backgroundColor = UIColor.clearColor;
fontLabel.textAlignment = NSTextAlignmentCenter;
} else {
fontLabel = (UILabel*)view;
@@ -1,65 +0,0 @@
//
// FLEXArgumentInputJSONObjectView.m
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputJSONObjectView.h"
#import "FLEXRuntimeUtility.h"
@implementation FLEXArgumentInputJSONObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
// square brackets are likely to be the first characters type for the JSON.
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeLarge;
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:inputValue];
}
- (id)inputValue
{
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
// Must be object type.
BOOL supported = type && type[0] == '@';
if (supported) {
if (value) {
// If there's a current value, it must be serializable to JSON
supported = [FLEXRuntimeUtility editableJSONStringForObject:value] != nil;
} else {
// Otherwise, see if we have more type information than just 'id'.
// If we do, make sure the encoding is something serializable to JSON.
// Properties and ivars keep more detailed type encoding information than method arguments.
if (strcmp(type, @encode(id)) != 0) {
BOOL isJSONSerializableType = NO;
// Note: we can't use @encode(NSString) here because that drops the string information and just goes to @encode(id).
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSString)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSNumber)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSArray)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSDictionary)) == 0;
supported = isJSONSerializableType;
}
}
}
return supported;
}
@end
@@ -30,7 +30,7 @@
- (id)inputValue
{
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
@@ -49,7 +49,8 @@
@(@encode(unsigned long)),
@(@encode(unsigned long long)),
@(@encode(float)),
@(@encode(double))];
@(@encode(double)),
@(@encode(long double))];
});
return type && [primitiveTypes containsObject:@(type)];
}
@@ -1,5 +1,5 @@
//
// FLEXArgumentInputJSONObjectView.h
// FLEXArgumentInputObjectView.h
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
@@ -8,6 +8,6 @@
#import "FLEXArgumentInputTextView.h"
@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
@end
@@ -0,0 +1,243 @@
//
// FLEXArgumentInputJSONObjectView.m
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputObjectView.h"
#import "FLEXRuntimeUtility.h"
static const CGFloat kSegmentInputMargin = 10;
typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
FLEXArgInputObjectTypeJSON,
FLEXArgInputObjectTypeAddress
};
@interface FLEXArgumentInputObjectView ()
@property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
@property (nonatomic) FLEXArgInputObjectType inputType;
@end
@implementation FLEXArgumentInputObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
// square brackets are likely to be the first characters type for the JSON.
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeLarge;
self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
[self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
self.objectTypeSegmentControl.selectedSegmentIndex = 0;
[self addSubview:self.objectTypeSegmentControl];
self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
}
return self;
}
- (void)didChangeType
{
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
if (super.inputValue) {
// Trigger an update to the text field to show
// the address of the stored object we were given,
// or to show a JSON representation of the object
[self populateTextAreaFromValue:super.inputValue];
} else {
// Clear the text field
[self populateTextAreaFromValue:nil];
}
}
- (void)setInputType:(FLEXArgInputObjectType)inputType
{
if (_inputType == inputType) return;
_inputType = inputType;
// Resize input view
switch (inputType) {
case FLEXArgInputObjectTypeJSON:
self.targetSize = FLEXArgumentInputViewSizeLarge;
break;
case FLEXArgInputObjectTypeAddress:
self.targetSize = FLEXArgumentInputViewSizeSmall;
break;
}
// Change placeholder
switch (inputType) {
case FLEXArgInputObjectTypeJSON:
self.inputPlaceholderText =
@"You can put any valid JSON here, such as a string, number, array, or dictionary:"
"\n\"This is a string\""
"\n1234"
"\n{ \"name\": \"Bob\", \"age\": 47 }"
"\n["
"\n 1, 2, 3"
"\n]";
break;
case FLEXArgInputObjectTypeAddress:
self.inputPlaceholderText = @"0x0000deadb33f";
break;
}
[self setNeedsLayout];
[self.superview setNeedsLayout];
}
- (void)setInputValue:(id)inputValue
{
super.inputValue = inputValue;
[self populateTextAreaFromValue:inputValue];
}
- (id)inputValue
{
switch (self.inputType) {
case FLEXArgInputObjectTypeJSON:
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
case FLEXArgInputObjectTypeAddress: {
NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
unsigned long long objectPointerValue;
if ([scanner scanHexLongLong:&objectPointerValue]) {
return (__bridge id)(void *)objectPointerValue;
}
return nil;
}
}
}
- (void)populateTextAreaFromValue:(id)value
{
if (!value) {
self.inputTextView.text = nil;
} else {
if (self.inputType == FLEXArgInputObjectTypeJSON) {
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
} else if (self.inputType == FLEXArgInputObjectTypeAddress) {
self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
}
}
// Delegate methods are not called for programmatic changes
[self textViewDidChange:self.inputTextView];
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
return fitSize;
}
- (void)layoutSubviews
{
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
self.objectTypeSegmentControl.frame = CGRectMake(
0.0,
// Our segmented control is taking the position
// of the text view, as far as super is concerned,
// and we override this property to be different
super.topInputFieldVerticalLayoutGuide,
self.frame.size.width,
segmentHeight
);
[super layoutSubviews];
}
- (CGFloat)topInputFieldVerticalLayoutGuide
{
// Our text view is offset from the segmented control
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
NSParameterAssert(type);
// Must be object type
return type[0] == '@';
}
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value
{
NSParameterAssert(type[0] == '@');
if (value) {
// If there's a current value, it must be serializable to JSON
// to display the JSON editor. Otherwise display the address field.
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
return FLEXArgInputObjectTypeJSON;
} else {
return FLEXArgInputObjectTypeAddress;
}
} else {
// Otherwise, see if we have more type information than just 'id'.
// If we do, make sure the encoding is something serializable to JSON.
// Properties and ivars keep more detailed type encoding information than method arguments.
if (strcmp(type, @encode(id)) != 0) {
BOOL isJSONSerializableType = NO;
// Parse class name out of the string,
// which is in the form `@"ClassName"`
Class cls = NSClassFromString(({
NSString *className = nil;
NSScanner *scan = [NSScanner scannerWithString:@(type)];
NSCharacterSet *allowed = [NSCharacterSet
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
];
// Skip over the @" then scan the name
if ([scan scanString:@"@\"" intoString:nil]) {
[scan scanCharactersFromSet:allowed intoString:&className];
}
className;
}));
// Note: we can't use @encode(NSString) here because that drops
// the class information and just goes to @encode(id).
NSArray<Class> *jsonTypes = @[
[NSString class],
[NSNumber class],
[NSArray class],
[NSDictionary class],
];
// Look for matching types
for (Class jsonClass in jsonTypes) {
if ([cls isSubclassOfClass:jsonClass]) {
isJSONSerializableType = YES;
break;
}
}
if (isJSONSerializableType) {
return FLEXArgInputObjectTypeJSON;
} else {
return FLEXArgInputObjectTypeAddress;
}
} else {
return FLEXArgInputObjectTypeAddress;
}
}
}
@end
@@ -27,11 +27,12 @@
- (id)inputValue
{
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
// but we accept that tradeoff in exchange for not having to type quotes for every string.
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
// TODO: Support using object address for strings, as in the object arg view.
#pragma mark -
@@ -12,7 +12,7 @@
@interface FLEXArgumentInputStructView ()
@property (nonatomic, strong) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
@@ -30,7 +30,7 @@
inputView.backgroundColor = self.backgroundColor;
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
if (fieldIndex < [customTitles count]) {
if (fieldIndex < customTitles.count) {
inputView.title = customTitles[fieldIndex];
} else {
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
@@ -59,7 +59,7 @@
{
if ([inputValue isKindOfClass:[NSValue class]]) {
const char *structTypeEncoding = [inputValue objCType];
if (strcmp([self.typeEncoding UTF8String], structTypeEncoding) == 0) {
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
NSUInteger valueSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
@@ -74,7 +74,7 @@
void *fieldPointer = unboxedValue + fieldOffset;
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
inputView.inputValue = (__bridge id)fieldPointer;
} else {
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
@@ -90,7 +90,7 @@
- (id)inputValue
{
NSValue *boxedStruct = nil;
const char *structTypeEncoding = [self.typeEncoding UTF8String];
const char *structTypeEncoding = self.typeEncoding.UTF8String;
NSUInteger structSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
@@ -104,7 +104,7 @@
void *fieldPointer = unboxedStruct + fieldOffset;
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
// Object fields
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
} else {
@@ -176,7 +176,7 @@
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return type && type[0] == '{';
return type && type[0] == FLEXTypeEncodingStructBegin;
}
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
@@ -188,6 +188,8 @@
customTitles = @[@"CGFloat x", @"CGFloat y"];
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
customTitles = @[@"CGFloat width", @"CGFloat height"];
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
@@ -203,6 +205,13 @@
customTitles = @[@"CGFloat a", @"CGFloat b",
@"CGFloat c", @"CGFloat d",
@"CGFloat tx", @"CGFloat ty"];
} else {
if (@available(iOS 11.0, *)) {
if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat leading",
@"CGFloat bottom", @"CGFloat trailing"];
}
}
}
return customTitles;
}
@@ -10,7 +10,7 @@
@interface FLEXArgumentInputSwitchView ()
@property (nonatomic, strong) UISwitch *inputSwitch;
@property (nonatomic) UISwitch *inputSwitch;
@end
@@ -20,7 +20,7 @@
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputSwitch = [[UISwitch alloc] init];
self.inputSwitch = [UISwitch new];
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
[self.inputSwitch sizeToFit];
[self addSubview:self.inputSwitch];
@@ -8,10 +8,11 @@
#import "FLEXArgumentInputView.h"
@interface FLEXArgumentInputTextView : FLEXArgumentInputView
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
// For subclass eyes only
@property (nonatomic, strong, readonly) UITextView *inputTextView;
@property (nonatomic, readonly) UITextView *inputTextView;
@property (nonatomic) NSString *inputPlaceholderText;
@end
@@ -6,12 +6,14 @@
//
//
#import "FLEXColor.h"
#import "FLEXArgumentInputTextView.h"
#import "FLEXUtility.h"
@interface FLEXArgumentInputTextView () <UITextViewDelegate>
@interface FLEXArgumentInputTextView ()
@property (nonatomic, strong) UITextView *inputTextView;
@property (nonatomic) UITextView *inputTextView;
@property (nonatomic) UILabel *placeholderLabel;
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
@end
@@ -22,23 +24,31 @@
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView = [[UITextView alloc] init];
self.inputTextView = [UITextView new];
self.inputTextView.font = [[self class] inputFont];
self.inputTextView.backgroundColor = [UIColor whiteColor];
self.inputTextView.layer.borderColor = [[UIColor blackColor] CGColor];
self.inputTextView.layer.borderWidth = 1.0;
self.inputTextView.backgroundColor = [FLEXColor primaryBackgroundColor];
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.cornerRadius = 5.f;
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
[self addSubview:self.inputTextView];
self.placeholderLabel = [UILabel new];
self.placeholderLabel.font = self.inputTextView.font;
self.placeholderLabel.textColor = [FLEXColor deemphasizedTextColor];
self.placeholderLabel.numberOfLines = 0;
[self.inputTextView addSubview:self.placeholderLabel];
}
return self;
}
#pragma mark - private
#pragma mark - Private
- (UIToolbar*)createToolBar
- (UIToolbar *)createToolBar
{
UIToolbar *toolBar = [UIToolbar new];
[toolBar sizeToFit];
@@ -53,12 +63,25 @@
[self.inputTextView resignFirstResponder];
}
#pragma mark - Text View Changes
- (void)textViewDidChange:(UITextView *)textView
- (void)setInputPlaceholderText:(NSString *)placeholder
{
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.text = placeholder;
if (placeholder.length) {
if (!self.inputTextView.text.length) {
self.placeholderLabel.hidden = NO;
} else {
self.placeholderLabel.hidden = YES;
}
} else {
self.placeholderLabel.hidden = YES;
}
[self setNeedsLayout];
}
- (NSString *)inputPlaceholderText
{
return self.placeholderLabel.text;
}
@@ -77,25 +100,28 @@
[super layoutSubviews];
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
// Placeholder label is positioned by insetting origin,
// which is the line fragment padding for X and 0 for Y,
// by the content inset then the text container inset
CGFloat leading = self.inputTextView.textContainer.lineFragmentPadding;
CGSize s = self.inputTextView.frame.size;
self.placeholderLabel.frame = CGRectMake(leading, 0, s.width, s.height);
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
self.inputTextView.textContainerInset
);
}
- (NSUInteger)numberOfInputLines
{
NSUInteger numberOfInputLines = 0;
switch (self.targetSize) {
case FLEXArgumentInputViewSizeDefault:
numberOfInputLines = 2;
break;
return 2;
case FLEXArgumentInputViewSizeSmall:
numberOfInputLines = 1;
break;
return 1;
case FLEXArgumentInputViewSizeLarge:
numberOfInputLines = 8;
break;
return 8;
}
return numberOfInputLines;
}
- (CGFloat)inputTextViewHeight
@@ -111,6 +137,20 @@
}
#pragma mark - Trait collection changes
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
}
}
#endif
}
#pragma mark - Class Helpers
+ (UIFont *)inputFont
@@ -118,4 +158,13 @@
return [FLEXUtility defaultFontOfSize:14.0];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView
{
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
}
@end
@@ -26,12 +26,13 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
/// To populate the filed with an initial value, set this property.
/// To reteive the value input by the user, access the property.
/// Primitive types and structs should/will be boxed in NSValue containers.
/// Concrete subclasses *must* override both the setter and getter for this property.
/// Concrete subclasses should override both the setter and getter for this property.
/// Subclasses can call super.inputValue to access a backing store for the value.
@property (nonatomic) id inputValue;
/// Setting this value to large will make some argument input views increase the size of their input field(s).
/// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
@property (nonatomic, assign) FLEXArgumentInputViewSize targetSize;
@property (nonatomic) FLEXArgumentInputViewSize targetSize;
/// Users of the input view can get delegate callbacks for incremental changes in user input.
@property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate;
@@ -47,8 +48,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
// For subclass eyes only
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) NSString *typeEncoding;
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic, readonly) NSString *typeEncoding;
@property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide;
@end
@@ -11,8 +11,8 @@
@interface FLEXArgumentInputView ()
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) NSString *typeEncoding;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) NSString *typeEncoding;
@end
@@ -56,7 +56,7 @@
- (UILabel *)titleLabel
{
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel = [UILabel new];
_titleLabel.font = [[self class] titleFont];
_titleLabel.backgroundColor = self.backgroundColor;
_titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
@@ -68,7 +68,7 @@
- (BOOL)showsTitle
{
return [self.title length] > 0;
return self.title.length > 0;
}
- (CGFloat)topInputFieldVerticalLayoutGuide
@@ -89,17 +89,6 @@
return NO;
}
- (void)setInputValue:(id)inputValue
{
// Subclasses should override.
}
- (id)inputValue
{
// Subclasses should override.
return nil;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return NO;
@@ -125,7 +114,7 @@
{
CGFloat height = 0;
if ([self.title length] > 0) {
if (self.title.length > 0) {
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
height += ceil([self.titleLabel sizeThatFits:constrainSize].height);
height += [[self class] titleBottomPadding];
@@ -8,7 +8,7 @@
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputJSONObjectView.h"
#import "FLEXArgumentInputObjectView.h"
#import "FLEXArgumentInputNumberView.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXArgumentInputStructView.h"
@@ -17,6 +17,7 @@
#import "FLEXArgumentInputFontView.h"
#import "FLEXArgumentInputColorView.h"
#import "FLEXArgumentInputDateView.h"
#import "FLEXRuntimeUtility.h"
@implementation FLEXArgumentInputViewFactory
@@ -33,34 +34,35 @@
// The unsupported view shows "nil" and does not allow user input.
subclass = [FLEXArgumentInputNotSupportedView class];
}
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding];
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset];
}
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
Class argumentInputViewSubclass = nil;
NSArray<Class> *inputViewClasses = @[[FLEXArgumentInputColorView class],
[FLEXArgumentInputFontView class],
[FLEXArgumentInputStringView class],
[FLEXArgumentInputStructView class],
[FLEXArgumentInputSwitchView class],
[FLEXArgumentInputDateView class],
[FLEXArgumentInputNumberView class],
[FLEXArgumentInputObjectView class]];
// Note that order is important here since multiple subclasses may support the same type.
// An example is the number subclass and the bool subclass for the type @encode(BOOL).
// Both work, but we'd prefer to use the bool subclass.
if ([FLEXArgumentInputColorView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputColorView class];
} else if ([FLEXArgumentInputFontView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputFontView class];
} else if ([FLEXArgumentInputStringView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputStringView class];
} else if ([FLEXArgumentInputStructView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputStructView class];
} else if ([FLEXArgumentInputSwitchView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputSwitchView class];
} else if ([FLEXArgumentInputDateView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputDateView class];
} else if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
} else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputJSONObjectView class];
for (Class inputViewClass in inputViewClasses) {
if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) {
argumentInputViewSubclass = inputViewClass;
break;
}
}
return argumentInputViewSubclass;
}
@@ -6,9 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXMutableFieldEditorViewController.h"
@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
@interface FLEXDefaultEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
@@ -15,7 +15,7 @@
@interface FLEXDefaultEditorViewController ()
@property (nonatomic, readonly) NSUserDefaults *defaults;
@property (nonatomic, strong) NSString *key;
@property (nonatomic) NSString *key;
@end
@@ -43,7 +43,10 @@
self.fieldEditorView.fieldDescription = self.key;
id currentValue = [self.defaults objectForKey:self.key];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id) currentValue:currentValue];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = currentValue;
self.fieldEditorView.argumentInputViews = @[inputView];
@@ -64,9 +67,19 @@
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [self.defaults objectForKey:self.key];
[self exploreObjectOrPopViewController:returnedObject];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
return [FLEXArgumentInputViewFactory
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
];
}
@end
+1 -1
View File
@@ -15,6 +15,6 @@
@property (nonatomic, copy) NSString *targetDescription;
@property (nonatomic, copy) NSString *fieldDescription;
@property (nonatomic, strong) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
+8 -8
View File
@@ -12,10 +12,10 @@
@interface FLEXFieldEditorView ()
@property (nonatomic, strong) UILabel *targetDescriptionLabel;
@property (nonatomic, strong) UIView *targetDescriptionDivider;
@property (nonatomic, strong) UILabel *fieldDescriptionLabel;
@property (nonatomic, strong) UIView *fieldDescriptionDivider;
@property (nonatomic) UILabel *targetDescriptionLabel;
@property (nonatomic) UIView *targetDescriptionDivider;
@property (nonatomic) UILabel *fieldDescriptionLabel;
@property (nonatomic) UIView *fieldDescriptionDivider;
@end
@@ -25,7 +25,7 @@
{
self = [super initWithFrame:frame];
if (self) {
self.targetDescriptionLabel = [[UILabel alloc] init];
self.targetDescriptionLabel = [UILabel new];
self.targetDescriptionLabel.numberOfLines = 0;
self.targetDescriptionLabel.font = [[self class] labelFont];
[self addSubview:self.targetDescriptionLabel];
@@ -33,7 +33,7 @@
self.targetDescriptionDivider = [[self class] dividerView];
[self addSubview:self.targetDescriptionDivider];
self.fieldDescriptionLabel = [[UILabel alloc] init];
self.fieldDescriptionLabel = [UILabel new];
self.fieldDescriptionLabel.numberOfLines = 0;
self.fieldDescriptionLabel.font = [[self class] labelFont];
[self addSubview:self.fieldDescriptionLabel];
@@ -123,14 +123,14 @@
+ (UIView *)dividerView
{
UIView *dividerView = [[UIView alloc] init];
UIView *dividerView = [UIView new];
dividerView.backgroundColor = [self dividerColor];
return dividerView;
}
+ (UIColor *)dividerColor
{
return [UIColor lightGrayColor];
return UIColor.lightGrayColor;
}
+ (CGFloat)horizontalPadding
@@ -19,10 +19,14 @@
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
// For subclass use only.
@property (nonatomic, strong, readonly) id target;
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
- (NSString *)titleForActionButton;
/// Pushes an explorer view controller for the given object
/// or pops the current view controller.
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
@end
+25 -10
View File
@@ -6,20 +6,23 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXObjectExplorerViewController.h"
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic, strong, readwrite) id target;
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
@property (nonatomic, readwrite) id target;
@property (nonatomic, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readwrite) UIBarButtonItem *setterButton;
@end
@@ -30,15 +33,15 @@
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.target = target;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[NSNotificationCenter.defaultCenter removeObserver:self];
}
- (void)keyboardDidShow:(NSNotification *)notification
@@ -72,7 +75,7 @@
{
[super viewDidLoad];
self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
self.view.backgroundColor = [FLEXColor scrollViewBackgroundColor];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
@@ -80,7 +83,7 @@
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
self.fieldEditorView = [FLEXFieldEditorView new];
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
@@ -99,7 +102,7 @@
- (FLEXArgumentInputView *)firstInputView
{
return [[self.fieldEditorView argumentInputViews] firstObject];
return [self.fieldEditorView argumentInputViews].firstObject;
}
- (void)actionButtonPressed:(id)sender
@@ -114,4 +117,16 @@
return @"Set";
}
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
if (objectOrNil) {
// For non-nil (or void) return types, push an explorer view controller to display the object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
@@ -6,10 +6,10 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
@interface FLEXIvarEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+15 -1
View File
@@ -15,7 +15,7 @@
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) Ivar ivar;
@property (nonatomic) Ivar ivar;
@end
@@ -52,9 +52,23 @@
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
// Pop view controller for consistency;
// property setters and method calls also pop on success.
[self.navigationController popViewControllerAnimated:YES];
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
@@ -13,10 +13,12 @@
#import "FLEXObjectExplorerViewController.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXUtility.h"
@interface FLEXMethodCallingViewController ()
@property (nonatomic, assign) Method method;
@property (nonatomic) Method method;
@property (nonatomic) FLEXTypeEncoding *returnType;
@end
@@ -27,6 +29,7 @@
self = [super initWithTarget:target];
if (self) {
self.method = method;
self.returnType = [FLEXRuntimeUtility returnTypeForMethod:method];
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
}
return self;
@@ -36,7 +39,11 @@
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSString *returnType = @((const char *)self.returnType);
NSString *methodDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSString *format = @"Signature:\n%@\n\nReturn Type:\n%@";
NSString *info = [NSString stringWithFormat:format, methodDescription, returnType];
self.fieldEditorView.fieldDescription = info;
NSArray<NSString *> *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray array];
@@ -54,6 +61,12 @@
self.fieldEditorView.argumentInputViews = argumentInputViews;
}
- (void)dealloc
{
free(self.returnType);
self.returnType = NULL;
}
- (BOOL)isClassMethod
{
return self.target && self.target == [self.target class];
@@ -82,18 +95,14 @@
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
if (error) {
NSString *title = @"Method Call Failed";
NSString *message = [error localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
[FLEXAlert showAlert:@"Method Call Failed" message:[error localizedDescription] from:self];
} else if (returnedObject) {
// For non-nil (or void) return types, push an explorer view controller to display the returned object
returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
[self exploreObjectOrPopViewController:returnedObject];
}
}
@@ -0,0 +1,18 @@
//
// FLEXMutableFieldEditorViewController.h
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
@interface FLEXMutableFieldEditorViewController : FLEXFieldEditorViewController
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
- (void)getterButtonPressed:(id)sender;
- (NSString *)titleForGetterButton;
@end
@@ -0,0 +1,36 @@
//
// FLEXMutableFieldEditorViewController.m
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
@interface FLEXMutableFieldEditorViewController ()
@property (nonatomic, readwrite) UIBarButtonItem *getterButton;
@end
@implementation FLEXMutableFieldEditorViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.getterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForGetterButton] style:UIBarButtonItemStyleDone target:self action:@selector(getterButtonPressed:)];
self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
}
- (void)getterButtonPressed:(id)sender {
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
- (NSString *)titleForGetterButton {
return @"Get";
}
@end
@@ -6,13 +6,13 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
@interface FLEXPropertyEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property;
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value;
@end
@@ -12,10 +12,11 @@
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXUtility.h"
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) objc_property_t property;
@property (nonatomic) objc_property_t property;
@end
@@ -37,9 +38,9 @@
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
self.setterButton.enabled = [[self class] canEditProperty:self.property currentValue:currentValue];
self.setterButton.enabled = [[self class] canEditProperty:self.property onObject:self.target currentValue:currentValue];
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:self.property] UTF8String];
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:self.property].UTF8String;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
@@ -62,10 +63,7 @@
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
NSString *title = @"Property Setter Failed";
NSString *message = [error localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
[FLEXAlert showAlert:@"Property Setter Failed" message:[error localizedDescription] from:self];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
} else {
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
@@ -76,6 +74,13 @@
}
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
@@ -83,11 +88,12 @@
}
}
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value
{
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:property].UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:property];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property] && (!setterSelector || ![object respondsToSelector:setterSelector]);
return canEditType && !isReadonly;
}
@@ -10,6 +10,7 @@
@protocol FLEXExplorerViewControllerDelegate;
/// A view controller that manages the FLEX toolbar.
@interface FLEXExplorerViewController : UIViewController
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
@@ -26,49 +26,43 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
@property (nonatomic) FLEXExplorerToolbar *explorerToolbar;
/// Tracks the currently active tool/mode
@property (nonatomic, assign) FLEXExplorerMode currentMode;
@property (nonatomic) FLEXExplorerMode currentMode;
/// Gesture recognizer for dragging a view in move mode
@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
@property (nonatomic) UIPanGestureRecognizer *movePanGR;
/// Gesture recognizer for showing additional details on the selected view
@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
@property (nonatomic) UITapGestureRecognizer *detailsTapGR;
/// Only valid while a move pan gesture is in progress.
@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
@property (nonatomic) CGRect selectedViewFrameBeforeDragging;
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic, assign) CGRect toolbarFrameBeforeDragging;
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
/// Borders of all the visible views in the hierarchy at the selection point.
/// The keys are NSValues with the correponding view (nonretained).
@property (nonatomic, strong) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
/// The keys are NSValues with the corresponding view (nonretained).
@property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
/// The actual views at the selection point with the deepest view last.
@property (nonatomic, strong) NSArray<UIView *> *viewsAtTapPoint;
@property (nonatomic) NSArray<UIView *> *viewsAtTapPoint;
/// The view that we're currently highlighting with an overlay and displaying details for.
@property (nonatomic, strong) UIView *selectedView;
@property (nonatomic) UIView *selectedView;
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic, strong) UIView *selectedViewOverlay;
@property (nonatomic) UIView *selectedViewOverlay;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture intput.
/// We need to become key after modal presentation so we can correctly capture input.
/// If we're just showing the toolbar, we want the main app's window to remain key so that we don't interfere with input, status bar, etc.
@property (nonatomic, strong) UIWindow *previousKeyWindow;
/// Similar to the previousKeyWindow property above, we need to track status bar styling if
/// the app doesn't use view controller based status bar management. When we present a modal,
/// we want to change the status bar style to UIStausBarStyleDefault. Before changing, we stash
/// the current style. On dismissal, we return the staus bar to the style that the app was using previously.
@property (nonatomic, assign) UIStatusBarStyle previousStatusBarStyle;
@property (nonatomic) UIWindow *previousKeyWindow;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic, strong) NSMutableSet<UIView *> *observedViews;
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
@end
@@ -93,9 +87,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)viewDidLoad
{
[super viewDidLoad];
// Toolbar
self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
self.explorerToolbar = [FLEXExplorerToolbar new];
// Start the toolbar off below any bars that may be at the top of the view.
id toolbarOriginYDefault = [[NSUserDefaults standardUserDefaults] objectForKey:kFLEXToolbarTopMarginDefaultsKey];
@@ -131,8 +125,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (UIViewController *)viewControllerForRotationAndOrientation
{
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
UIWindow *window = self.previousKeyWindow ?: [UIApplication.sharedApplication keyWindow];
UIViewController *viewController = window.rootViewController;
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
@@ -174,25 +169,25 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
}
}
for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
}
}
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}];
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}];
}
#pragma mark - Setter Overrides
@@ -214,13 +209,13 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
if (selectedView) {
if (!self.selectedViewOverlay) {
self.selectedViewOverlay = [[UIView alloc] init];
self.selectedViewOverlay = [UIView new];
[self.view addSubview:self.selectedViewOverlay];
self.selectedViewOverlay.layer.borderWidth = 1.0;
}
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
self.selectedViewOverlay.layer.borderColor = [outlineColor CGColor];
self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
// Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
@@ -396,7 +391,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (UIWindow *)statusWindow
{
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [[UIApplication sharedApplication] valueForKey:statusBarString];
return [UIApplication.sharedApplication valueForKey:statusBarString];
}
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
@@ -447,12 +442,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
[self updateToolbarPostionWithDragGesture:panGR];
[self updateToolbarPositionWithDragGesture:panGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateToolbarPostionWithDragGesture:panGR];
[self updateToolbarPositionWithDragGesture:panGR];
break;
default:
@@ -460,7 +455,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR
{
CGPoint translation = [panGR translationInView:self.view];
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
@@ -561,8 +556,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
outlineView.backgroundColor = [UIColor clearColor];
outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
outlineView.backgroundColor = UIColor.clearColor;
outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
outlineView.layer.borderWidth = 1.0;
return outlineView;
}
@@ -593,8 +588,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
// Default to the the application's key window if none of the windows want the touch.
UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
for (UIWindow *window in [[FLEXUtility allWindows] reverseObjectEnumerator]) {
UIWindow *windowForSelection = [UIApplication.sharedApplication keyWindow];
for (UIWindow *window in [FLEXUtility allWindows].reverseObjectEnumerator) {
// Ignore the explorer's own window.
if (window != self.view.window) {
if ([window hitTest:tapPointInWindow withEvent:nil]) {
@@ -605,7 +600,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
return [[self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES] lastObject];
return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
}
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
@@ -693,26 +688,23 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (CGRect)viewSafeArea
{
CGRect safeArea = self.view.bounds;
#if FLEX_AT_LEAST_IOS11_SDK
if (@available(iOS 11, *)) {
if (@available(iOS 11.0, *)) {
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
}
#endif
return safeArea;
}
#if FLEX_AT_LEAST_IOS11_SDK
- (void)viewSafeAreaInsetsDidChange
{
if (@available(iOS 11, *)) {
[super viewSafeAreaInsetsDidChange];
}
if (@available(iOS 11.0, *)) {
[super viewSafeAreaInsetsDidChange];
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(self.explorerToolbar.frame), CGRectGetMinY(self.explorerToolbar.frame), toolbarSize.width, toolbarSize.height)];
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(self.explorerToolbar.frame), CGRectGetMinY(self.explorerToolbar.frame), toolbarSize.width, toolbarSize.height)];
}
}
#endif
#pragma mark - Touch Handling
@@ -793,20 +785,21 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
{
// Save the current key window so we can restore it following dismissal.
self.previousKeyWindow = [[UIApplication sharedApplication] keyWindow];
self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
// Make our window key to correctly handle input.
[self.view.window makeKeyWindow];
// Fix for iOS 13, regarding custom UIMenu callouts not appearing because
// the UITextEffectsWindow has a lower level than the FLEX window by default
// until a text field is activated, bringing it above the FLEX window.
if (@available(iOS 13, *)) {
UIApplication.sharedApplication.windows[1].windowLevel = self.view.window.windowLevel + 1;
}
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
// for view controller based management because the global methods no-op if that is turned on.
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
}
@@ -822,9 +815,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
// Restore the stauts bar style if the app is using global status bar management.
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
[self dismissViewControllerAnimated:animated completion:completion];
}
@@ -837,7 +827,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.presentedViewController) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:completion];
} else {
} else if (future) {
[self makeKeyAndPresentViewController:future() animated:YES completion:completion];
}
}
@@ -885,9 +875,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)toggleMenuTool
{
[self toggleToolWithViewControllerProvider:^UIViewController *{
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
FLEXGlobalsTableViewController *globalsViewController = [FLEXGlobalsTableViewController new];
globalsViewController.delegate = self;
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
[FLEXGlobalsTableViewController setApplicationWindow:[UIApplication.sharedApplication keyWindow]];
return [[UINavigationController alloc] initWithRootViewController:globalsViewController];
} completion:nil];
}
@@ -896,9 +886,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / [[UIScreen mainScreen] scale];
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
@@ -910,11 +900,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / [[UIScreen mainScreen] scale];
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex < [self.viewsAtTapPoint count] - 1) {
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
}
@@ -924,7 +914,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / [[UIScreen mainScreen] scale];
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
}
}
@@ -933,7 +923,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / [[UIScreen mainScreen] scale];
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
}
}
+2 -2
View File
@@ -15,7 +15,7 @@
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.backgroundColor = UIColor.clearColor;
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
@@ -49,7 +49,7 @@
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
// Done at runtime with an obfuscated selector because it is private API. But you shoudn't ship this to the App Store anyways...
// Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways...
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
+12 -4
View File
@@ -9,6 +9,10 @@
#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
@@ -21,16 +25,20 @@ typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
- (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, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
@property (nonatomic, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
/// Defaults to 25 MB if never set. Values set here are persisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
@@ -44,7 +52,7 @@ typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
@property (nonatomic) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
@@ -13,14 +13,14 @@
@interface FLEXMultiColumnTableView ()
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
@property (nonatomic, strong) UIScrollView *contentScrollView;
@property (nonatomic, strong) UIScrollView *headerScrollView;
@property (nonatomic, strong) UITableView *leftTableView;
@property (nonatomic, strong) UITableView *contentTableView;
@property (nonatomic, strong) UIView *leftHeader;
@property (nonatomic) UIScrollView *contentScrollView;
@property (nonatomic) UIScrollView *headerScrollView;
@property (nonatomic) UITableView *leftTableView;
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
@property (nonatomic, strong) NSArray *rowData;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
@property (nonatomic) NSArray *rowData;
@end
static const CGFloat kColumnMargin = 1;
@@ -51,6 +51,11 @@ static const CGFloat kColumnMargin = 1;
CGFloat height = self.frame.size.height;
CGFloat topheaderHeight = [self topHeaderHeight];
CGFloat leftHeaderWidth = [self leftHeaderWidth];
CGFloat topInsets = 0.f;
if (@available (iOS 11.0, *)) {
topInsets = self.safeAreaInsets.top;
}
CGFloat contentWidth = 0.0;
NSInteger rowsCount = [self numberOfColumns];
@@ -58,13 +63,13 @@ static const CGFloat kColumnMargin = 1;
contentWidth += [self contentWidthForColumn:i];
}
self.leftTableView.frame = CGRectMake(0, topheaderHeight, leftHeaderWidth, height - topheaderHeight);
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, 0, width - leftHeaderWidth, topheaderHeight);
self.leftTableView.frame = CGRectMake(0, topheaderHeight + topInsets, leftHeaderWidth, height - topheaderHeight - topInsets);
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight);
self.headerScrollView.contentSize = CGSizeMake( self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height);
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight);
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight, width - leftHeaderWidth, height - topheaderHeight);
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight - topInsets);
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, height - topheaderHeight - topInsets);
self.contentScrollView.contentSize = self.contentTableView.frame.size;
self.leftHeader.frame = CGRectMake(0, 0, [self leftHeaderWidth], [self topHeaderHeight]);
self.leftHeader.frame = CGRectMake(0, topInsets, [self leftHeaderWidth], [self topHeaderHeight]);
}
@@ -86,7 +91,7 @@ static const CGFloat kColumnMargin = 1;
- (void)loadHeaderScrollView
{
UIScrollView *headerScrollView = [[UIScrollView alloc] init];
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
self.headerScrollView = headerScrollView;
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
@@ -97,11 +102,11 @@ static const CGFloat kColumnMargin = 1;
- (void)loadContentScrollView
{
UIScrollView *scrollView = [[UIScrollView alloc] init];
UIScrollView *scrollView = [UIScrollView new];
scrollView.bounces = NO;
scrollView.delegate = self;
UITableView *tableView = [[UITableView alloc] init];
UITableView *tableView = [UITableView new];
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
@@ -116,14 +121,14 @@ static const CGFloat kColumnMargin = 1;
- (void)loadLeftView
{
UITableView *leftTableView = [[UITableView alloc] init];
UITableView *leftTableView = [UITableView new];
leftTableView.delegate = self;
leftTableView.dataSource = self;
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.leftTableView = leftTableView;
[self addSubview:leftTableView];
UIView *leftHeader = [[UIView alloc] init];
UIView *leftHeader = [UIView new];
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
self.leftHeader = leftHeader;
[self addSubview:leftHeader];
@@ -199,7 +204,7 @@ static const CGFloat kColumnMargin = 1;
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIColor *backgroundColor = [UIColor whiteColor];
UIColor *backgroundColor = UIColor.whiteColor;
if (indexPath.row % 2 != 0) {
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
}
@@ -214,11 +219,11 @@ static const CGFloat kColumnMargin = 1;
for (int i = 0 ; i < cell.labels.count; i++) {
UILabel *label = cell.labels[i];
label.textColor = [UIColor blackColor];
label.textColor = UIColor.blackColor;
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
if ([content isEqualToString:@"<null>"]) {
label.textColor = [UIColor lightGrayColor];
label.textColor = UIColor.lightGrayColor;
content = @"NULL";
}
label.text = content;
@@ -282,7 +287,7 @@ static const CGFloat kColumnMargin = 1;
#pragma mark -
#pragma mark DataSource Accessor
- (NSInteger)numberOfrows
- (NSInteger)numberOfRows
{
return [self.dataSource numberOfRowsInTableView:self];
}
@@ -18,7 +18,7 @@
@interface FLEXRealmDatabaseManager ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic, strong) RLMRealm * realm;
@property (nonatomic) RLMRealm * realm;
@end
@@ -51,7 +51,7 @@
}
NSError *error = nil;
id configuration = [[configurationClass alloc] init];
id configuration = [configurationClass new];
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
return (error == nil);
@@ -33,7 +33,7 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
if (_db) {
return YES;
}
int err = sqlite3_open([_databasePath UTF8String], &_db);
int err = sqlite3_open(_databasePath.UTF8String, &_db);
#if SQLITE_HAS_CODEC
NSString *defaultSqliteDatabasePassword = [FLEXManager sharedManager].defaultSqliteDatabasePassword;
@@ -117,7 +117,7 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
[self open];
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pstmt, 0) == SQLITE_OK) {
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
if (num_cols > 0) {
@@ -1,6 +1,6 @@
//
// FLEXTableContentHeaderCell.h
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
@@ -16,7 +16,7 @@ typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
@interface FLEXTableColumnHeader : UIView
@property (nonatomic, strong) UILabel *label;
@property (nonatomic) UILabel *label;
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
@@ -1,6 +1,6 @@
//
// FLEXTableContentHeaderCell.m
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
@@ -18,7 +18,7 @@
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
self.backgroundColor = UIColor.whiteColor;
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
label.font = [UIFont systemFontOfSize:13.0];
@@ -1,6 +1,6 @@
//
// FLEXTableContentCell.h
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
@@ -18,7 +18,7 @@
@interface FLEXTableContentCell : UITableViewCell
@property (nonatomic, strong) NSArray<UILabel *> *labels;
@property (nonatomic) NSArray<UILabel *> *labels;
@property (nonatomic, weak) id<FLEXTableContentCellDelegate> delegate;
@@ -1,6 +1,6 @@
//
// FLEXTableContentCell.m
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
@@ -24,10 +24,10 @@
NSMutableArray<UILabel *> *labels = [NSMutableArray array];
for (int i = 0; i < number ; i++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = [UIColor whiteColor];
label.backgroundColor = UIColor.whiteColor;
label.font = [UIFont systemFontOfSize:13.0];
label.textAlignment = NSTextAlignmentLeft;
label.backgroundColor = [UIColor greenColor];
label.backgroundColor = UIColor.greenColor;
[labels addObject:label];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
@@ -36,7 +36,7 @@
label.userInteractionEnabled = YES;
[cell.contentView addSubview:label];
cell.contentView.backgroundColor = [UIColor whiteColor];
cell.contentView.backgroundColor = UIColor.whiteColor;
}
cell.labels = labels;
}
@@ -10,7 +10,7 @@
@interface FLEXTableContentViewController : UIViewController
@property (nonatomic, strong) NSArray<NSString *> *columnsArray;
@property (nonatomic, strong) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
@property (nonatomic) NSArray<NSString *> *columnsArray;
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
@end
@@ -13,7 +13,7 @@
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
@property (nonatomic, strong) FLEXMultiColumnTableView *multiColumView;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@@ -22,29 +22,29 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self.view addSubview:self.multiColumView];
[self.view addSubview:self.multiColumnView];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.multiColumView reloadData];
[self.multiColumnView reloadData];
}
#pragma mark -
#pragma mark init SubView
- (FLEXMultiColumnTableView *)multiColumView {
if (!_multiColumView) {
_multiColumView = [[FLEXMultiColumnTableView alloc] initWithFrame:
- (FLEXMultiColumnTableView *)multiColumnView {
if (!_multiColumnView) {
_multiColumnView = [[FLEXMultiColumnTableView alloc] initWithFrame:
CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
_multiColumView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
_multiColumView.backgroundColor = [UIColor whiteColor];
_multiColumView.dataSource = self;
_multiColumView.delegate = self;
_multiColumnView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
_multiColumnView.backgroundColor = UIColor.whiteColor;
_multiColumnView.dataSource = self;
_multiColumnView.delegate = self;
}
return _multiColumView;
return _multiColumnView;
}
#pragma mark MultiColumnTableView DataSource
@@ -151,12 +151,12 @@
return result;
}];
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
NSEnumerator *contentReverseEvumerator = [sortContentData reverseObjectEnumerator];
sortContentData = [NSArray arrayWithArray:[contentReverseEvumerator allObjects]];
NSEnumerator *contentReverseEnumerator = sortContentData.reverseObjectEnumerator;
sortContentData = [NSArray arrayWithArray:contentReverseEnumerator.allObjects];
}
self.contentsArray = sortContentData;
[self.multiColumView reloadData];
[self.multiColumnView reloadData];
}
#pragma mark -
@@ -170,10 +170,10 @@
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
self->_multiColumView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
self->_multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
}
else {
self->_multiColumView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
self->_multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
}
[self.view setNeedsLayout];
} completion:nil];
@@ -1,6 +1,6 @@
//
// FLEXTableLeftCell.h
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
@@ -10,7 +10,7 @@
@interface FLEXTableLeftCell : UITableViewCell
@property (nonatomic, strong) UILabel *titlelabel;
@property (nonatomic) UILabel *titlelabel;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@@ -1,6 +1,6 @@
//
// FLEXTableLeftCell.m
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
@@ -20,7 +20,7 @@
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
textLabel.textAlignment = NSTextAlignmentCenter;
textLabel.font = [UIFont systemFontOfSize:13.0];
textLabel.backgroundColor = [UIColor clearColor];
textLabel.backgroundColor = UIColor.clearColor;
[cell.contentView addSubview:textLabel];
cell.titlelabel = textLabel;
}
@@ -6,9 +6,9 @@
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
@interface FLEXTableListViewController : UITableViewController
@interface FLEXTableListViewController : FLEXTableViewController
+ (BOOL)supportsExtension:(NSString *)extension;
- (instancetype)initWithPath:(NSString *)path;
@@ -20,7 +20,8 @@
NSString *_databasePath;
}
@property (nonatomic, strong) NSArray<NSString *> *tables;
@property (nonatomic) NSArray<NSString *> *tables;
@property (nonatomic) NSArray<NSString *> *filteredTables;
+ (NSArray<NSString *> *)supportedSQLiteExtensions;
+ (NSArray<NSString *> *)supportedRealmExtensions;
@@ -67,11 +68,35 @@
[array addObject:columnName];
}
self.tables = array;
self.filteredTables = array;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)searchText
{
if (searchText.length > 0) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredTables = [self.tables filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredTables = self.tables;
}
[self.tableView reloadData];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tables.count;
return self.filteredTables.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
@@ -81,28 +106,29 @@
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"FLEXTableListViewControllerCell"];
}
cell.textLabel.text = self.tables[indexPath.row];
cell.textLabel.text = self.filteredTables[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXTableContentViewController *contentViewController = [[FLEXTableContentViewController alloc] init];
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.tables[indexPath.row]];
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.tables[indexPath.row]];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.filteredTables[indexPath.row]];
contentViewController.title = self.tables[indexPath.row];
contentViewController.title = self.filteredTables[indexPath.row];
[self.navigationController pushViewController:contentViewController animated:YES];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [NSString stringWithFormat:@"%lu tables", (unsigned long)self.tables.count];
return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
}
#pragma mark - FLEXTableListViewController
+ (BOOL)supportsExtension:(NSString *)extension
{
extension = extension.lowercaseString;
@@ -0,0 +1,13 @@
//
// FLEXAddressExplorerCoordinator.h
// FLEX
//
// Created by Tanner Bennett on 7/10/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
@interface FLEXAddressExplorerCoordinator : NSObject <FLEXGlobalsEntry>
@end
@@ -0,0 +1,95 @@
//
// FLEXAddressExplorerCoordinator.m
// FLEX
//
// Created by Tanner Bennett on 7/10/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXGlobalsTableViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
@interface FLEXGlobalsTableViewController (FLEXAddressExploration)
- (void)deselectSelectedRow;
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely;
@end
@implementation FLEXAddressExplorerCoordinator
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🔎 Address Explorer";
}
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
return ^(FLEXGlobalsTableViewController *host) {
NSString *title = @"Explore Object at Address";
NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
"Use the unsafe option if you need to bypass pointer validation, "
"but know that it may crash the app if the address is invalid.";
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(title).message(message);
make.configuredTextField(^(UITextField *textField) {
NSString *copied = UIPasteboard.generalPasteboard.string;
textField.placeholder = @"0x00000070deadbeef";
// Go ahead and paste our clipboard if we have an address copied
if ([copied hasPrefix:@"0x"]) {
textField.text = copied;
[textField selectAll:nil];
}
});
make.button(@"Explore").handler(^(NSArray<NSString *> *strings) {
[host tryExploreAddress:strings.firstObject safely:YES];
});
make.button(@"Unsafe Explore").destructiveStyle().handler(^(NSArray *strings) {
[host tryExploreAddress:strings.firstObject safely:NO];
});
make.button(@"Cancel").cancelStyle();
} showFrom:host];
};
}
@end
@implementation FLEXGlobalsTableViewController (FLEXAddressExploration)
- (void)deselectSelectedRow {
NSIndexPath *selected = self.tableView.indexPathForSelectedRow;
[self.tableView deselectRowAtIndexPath:selected animated:YES];
}
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely {
NSScanner *scanner = [NSScanner scannerWithString:addressString];
unsigned long long hexValue = 0;
BOOL didParseAddress = [scanner scanHexLongLong:&hexValue];
const void *pointerValue = (void *)hexValue;
NSString *error = nil;
if (didParseAddress) {
if (safely && ![FLEXRuntimeUtility pointerIsValidObjcObject:pointerValue]) {
error = @"The given address is unlikely to be a valid object.";
}
} else {
error = @"Malformed address. Make sure it's not too long and starts with '0x'.";
}
if (!error) {
id object = (__bridge id)pointerValue;
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
[self.navigationController pushViewController:explorer animated:YES];
} else {
[FLEXAlert showAlert:@"Uh-oh" message:error from:self];
[self deselectSelectedRow];
}
}
@end
@@ -6,9 +6,10 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXClassesTableViewController : UITableViewController
@interface FLEXClassesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@property (nonatomic, copy) NSString *binaryImageName;
@@ -12,11 +12,10 @@
#import "FLEXUtility.h"
#import <objc/runtime.h>
@interface FLEXClassesTableViewController () <UISearchBarDelegate>
@interface FLEXClassesTableViewController ()
@property (nonatomic, strong) NSArray<NSString *> *classNames;
@property (nonatomic, strong) NSArray<NSString *> *filteredClassNames;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic) NSArray<NSString *> *classNames;
@property (nonatomic) NSArray<NSString *> *filteredClassNames;
@end
@@ -26,11 +25,7 @@
{
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
self.searchBar.delegate = self;
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
self.showsSearchBar = YES;
}
- (void)setBinaryImageName:(NSString *)binaryImageName
@@ -51,7 +46,7 @@
- (void)loadClassNames
{
unsigned int classNamesCount = 0;
const char **classNames = objc_copyClassNamesForImage([self.binaryImageName UTF8String], &classNamesCount);
const char **classNames = objc_copyClassNamesForImage(self.binaryImageName.UTF8String, &classNamesCount);
if (classNames) {
NSMutableArray<NSString *> *classNameStrings = [NSMutableArray array];
for (unsigned int i = 0; i < classNamesCount; i++) {
@@ -69,17 +64,31 @@
- (void)updateTitle
{
NSString *shortImageName = self.binaryImageName.lastPathComponent;
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)", shortImageName, (unsigned long)[self.filteredClassNames count]];
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)", shortImageName, (unsigned long)self.filteredClassNames.count];
}
#pragma mark - Search
#pragma mark - FLEXGlobalsEntry
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXClassesTableViewController *classesViewController = [self new];
classesViewController.binaryImageName = [FLEXUtility applicationImageName];
return classesViewController;
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)searchText
{
if ([searchText length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPreidcate];
if (searchText.length > 0) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredClassNames = self.classNames;
}
@@ -87,17 +96,6 @@
[self.tableView reloadData];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Dismiss the keyboard when interacting with filtered results.
[self.searchBar endEditing:YES];
}
#pragma mark - Table View Data Source
@@ -108,7 +106,7 @@
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.filteredClassNames count];
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
@@ -132,7 +130,7 @@
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *className = self.filteredClassNames[indexPath.row];
Class selectedClass = objc_getClass([className UTF8String]);
Class selectedClass = objc_getClass(className.UTF8String);
FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
[self.navigationController pushViewController:objectExplorer animated:YES];
}
@@ -6,8 +6,9 @@
// Copyright © 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXGlobalsEntry.h"
#import "FLEXTableViewController.h"
@interface FLEXCookiesTableViewController : UITableViewController
@interface FLEXCookiesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -11,33 +11,37 @@
#import "FLEXUtility.h"
@interface FLEXCookiesTableViewController ()
@property (nonatomic, strong) NSArray<NSHTTPCookie *> *cookies;
@property (nonatomic, readonly) NSArray<NSHTTPCookie *> *cookies;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXCookiesTableViewController
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
self.title = @"Cookies";
- (void)viewDidLoad {
[super viewDidLoad];
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)];
_cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies sortedArrayUsingDescriptors:@[nameSortDescriptor]];
}
return self;
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
];
_cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
sortedArrayUsingDescriptors:@[nameSortDescriptor]
];
self.title = @"Cookies";
[self updateHeaderTitle];
}
- (void)updateHeaderTitle {
self.headerTitle = [NSString stringWithFormat:@"%@ cookies", @(self.cookies.count)];
// TODO update header title here when we can search cookies
}
- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.cookies[indexPath.row];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.cookies.count;
@@ -50,7 +54,7 @@
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.detailTextLabel.textColor = UIColor.grayColor;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
@@ -61,6 +65,13 @@
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.headerTitle;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
@@ -68,4 +79,15 @@
[self.navigationController pushViewController:cookieViewController animated:YES];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🍪 Cookies";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self new];
}
@end
@@ -1,33 +0,0 @@
//
// FLEXFileBrowserFileOperationController.h
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/13/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol FLEXFileBrowserFileOperationController;
@protocol FLEXFileBrowserFileOperationControllerDelegate <NSObject>
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller;
@end
@protocol FLEXFileBrowserFileOperationController <NSObject>
@property (nonatomic, weak) id<FLEXFileBrowserFileOperationControllerDelegate> delegate;
- (instancetype)initWithPath:(NSString *)path;
- (void)show;
@end
@interface FLEXFileBrowserFileDeleteOperationController : NSObject <FLEXFileBrowserFileOperationController>
@end
@interface FLEXFileBrowserFileRenameOperationController : NSObject <FLEXFileBrowserFileOperationController>
@end
@@ -1,142 +0,0 @@
//
// FLEXFileBrowserFileOperationController.m
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/13/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXFileBrowserFileOperationController.h"
#import <UIKit/UIKit.h>
@interface FLEXFileBrowserFileDeleteOperationController () <UIAlertViewDelegate>
@property (nonatomic, copy, readonly) NSString *path;
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
@end
@implementation FLEXFileBrowserFileDeleteOperationController
@synthesize delegate = _delegate;
- (instancetype)init
{
return [self initWithPath:nil];
}
- (instancetype)initWithPath:(NSString *)path
{
self = [super init];
if (self) {
_path = path;
}
return self;
}
- (void)show
{
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
if (stillExists) {
UIAlertView *deleteWarning = [[UIAlertView alloc]
initWithTitle:[NSString stringWithFormat:@"Delete %@?", self.path.lastPathComponent]
message:[NSString stringWithFormat:@"The %@ will be deleted. This operation cannot be undone", isDirectory ? @"directory" : @"file"]
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Delete", nil];
[deleteWarning show];
} else {
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == alertView.cancelButtonIndex) {
// Nothing, just cancel
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
[[NSFileManager defaultManager] removeItemAtPath:self.path error:NULL];
}
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
[self.delegate fileOperationControllerDidDismiss:self];
}
@end
@interface FLEXFileBrowserFileRenameOperationController () <UIAlertViewDelegate>
@property (nonatomic, copy, readonly) NSString *path;
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
@end
@implementation FLEXFileBrowserFileRenameOperationController
@synthesize delegate = _delegate;
- (instancetype)init
{
return [self initWithPath:nil];
}
- (instancetype)initWithPath:(NSString *)path
{
self = [super init];
if (self) {
_path = path;
}
return self;
}
- (void)show
{
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
if (stillExists) {
UIAlertView *renameDialog = [[UIAlertView alloc]
initWithTitle:[NSString stringWithFormat:@"Rename %@?", self.path.lastPathComponent]
message:nil
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Rename", nil];
renameDialog.alertViewStyle = UIAlertViewStylePlainTextInput;
UITextField *textField = [renameDialog textFieldAtIndex:0];
textField.placeholder = @"New file name";
textField.text = self.path.lastPathComponent;
[renameDialog show];
} else {
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == alertView.cancelButtonIndex) {
// Nothing, just cancel
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
NSString *newFileName = [alertView textFieldAtIndex:0].text;
NSString *newPath = [[self.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:newFileName];
[[NSFileManager defaultManager] moveItemAtPath:self.path toPath:newPath error:NULL];
}
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
[self.delegate fileOperationControllerDidDismiss:self];
}
@end
@@ -1,376 +0,0 @@
//
// FLEXFileBrowserTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXFileBrowserFileOperationController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXTableListViewController.h"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserFileOperationControllerDelegate, FLEXFileBrowserSearchOperationDelegate, UISearchResultsUpdating, UISearchControllerDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
@property (nonatomic, strong) NSArray<NSString *> *searchPaths;
@property (nonatomic, strong) NSNumber *recursiveSize;
@property (nonatomic, strong) NSNumber *searchPathsSize;
@property (nonatomic, strong) UISearchController *searchController;
@property (nonatomic) NSOperationQueue *operationQueue;
@property (nonatomic, strong) UIDocumentInteractionController *documentController;
@property (nonatomic, strong) id<FLEXFileBrowserFileOperationController> fileOperationController;
@end
@implementation FLEXFileBrowserTableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
return [self initWithPath:NSHomeDirectory()];
}
- (id)initWithPath:(NSString *)path
{
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
self.path = path;
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.searchResultsUpdater = self;
self.searchController.delegate = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.tableView.tableHeaderView = self.searchController.searchBar;
//computing path size
FLEXFileBrowserTableViewController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away.
if (!weakSelf) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
});
[self reloadChildPaths];
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
}
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size
{
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.tableView reloadData];
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
[self reloadDisplayedPaths];
}
#pragma mark - UISearchControllerDelegate
- (void)willDismissSearchController:(UISearchController *)searchController
{
[self.operationQueue cancelAllOperations];
[self reloadChildPaths];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.searchController.isActive ? [self.searchPaths count] : [self.childPaths count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
BOOL isSearchActive = self.searchController.isActive;
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
NSArray<NSString *> *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
NSString *sizeString = nil;
if (!currentSize) {
sizeString = @"Computing size…";
} else {
sizeString = [NSByteCountFormatter stringFromByteCount:[currentSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
}
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)[currentPaths count], sizeString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSDictionary<NSString *, id> *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:NULL];
BOOL isDirectory = [[attributes fileType] isEqual:NSFileTypeDirectory];
NSString *subtitle = nil;
if (isDirectory) {
NSUInteger count = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:fullPath error:NULL] count];
subtitle = [NSString stringWithFormat:@"%lu file%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
} else {
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleFile];
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, [attributes fileModificationDate]];
}
static NSString *textCellIdentifier = @"textCell";
static NSString *imageCellIdentifier = @"imageCell";
UITableViewCell *cell = nil;
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
BOOL showImagePreview = [FLEXUtility isImagePathExtension:[fullPath pathExtension]];
NSString *cellIdentifier = showImagePreview ? imageCellIdentifier : textCellIdentifier;
if (!cell) {
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSString *cellTitle = [fullPath lastPathComponent];
cell.textLabel.text = cellTitle;
cell.detailTextLabel.text = subtitle;
if (showImagePreview) {
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.imageView.image = [UIImage imageWithContentsOfFile:fullPath];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSString *subpath = [fullPath lastPathComponent];
NSString *pathExtension = [subpath pathExtension];
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if ([FLEXUtility isImagePathExtension:pathExtension]) {
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([pathExtension isEqual:@"archive"] || [pathExtension isEqual:@"coded"]) {
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
} else if ([pathExtension isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
} else if ([pathExtension isEqualToString:@"plist"]) {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
}
if ([prettyString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else if ([FLEXTableListViewController supportsExtension:subpath.pathExtension]) {
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
}
else {
NSString *fileString = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:NULL];
if ([fileString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
}
}
}
if (drillInViewController) {
drillInViewController.title = [subpath lastPathComponent];
[self.navigationController pushViewController:drillInViewController animated:YES];
} else {
[self openFileController:fullPath];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
} else {
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
[self reloadDisplayedPaths];
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIMenuItem *renameMenuItem = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *deleteMenuItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
NSMutableArray *menus = [NSMutableArray arrayWithObjects:renameMenuItem, deleteMenuItem, nil];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSError *error = nil;
NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:&error];
if (error == nil && [attributes fileType] != NSFileTypeDirectory) {
UIMenuItem *shareMenuItem = [[UIMenuItem alloc] initWithTitle:@"Share" action:@selector(fileBrowserShare:)];
[menus addObject:shareMenuItem];
}
[UIMenuController sharedMenuController].menuItems = menus;
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(fileBrowserDelete:) || action == @selector(fileBrowserRename:) || action == @selector(fileBrowserShare:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
// Empty, but has to exist for the menu to show
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
#pragma mark - FLEXFileBrowserFileOperationControllerDelegate
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller
{
[self reloadDisplayedPaths];
}
- (void)openFileController:(NSString *)fullPath
{
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [[NSURL alloc] initFileURLWithPath:fullPath];
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
self.documentController = controller;
}
- (void)fileBrowserRename:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
self.fileOperationController = [[FLEXFileBrowserFileRenameOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
self.fileOperationController = [[FLEXFileBrowserFileDeleteOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)fileBrowserShare:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[fullPath] applicationActivities:nil];
[self presentViewController:activityViewController animated:true completion:nil];
}
- (void)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self reloadSearchPaths];
} else {
[self reloadChildPaths];
}
[self.tableView reloadData];
}
- (void)reloadChildPaths
{
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
NSArray<NSString *> *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:NULL];
for (NSString *subpath in subpaths) {
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
}
self.childPaths = childPaths;
}
- (void)reloadSearchPaths
{
self.searchPaths = nil;
self.searchPathsSize = nil;
//clear pre search request and start a new one
[self.operationQueue cancelAllOperations];
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchController.searchBar.text];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
}
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
{
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
}
@end
@implementation FLEXFileBrowserTableViewCell
- (void)fileBrowserRename:(UIMenuController *)sender
{
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
}
- (void)fileBrowserDelete:(UIMenuController *)sender
{
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
}
- (void)fileBrowserShare:(UIMenuController *)sender
{
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
}
@end
@@ -1,315 +0,0 @@
//
// FLEXGlobalsTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXGlobalsTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXCookiesTableViewController.h"
#import "FLEXGlobalsTableViewControllerEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
static __weak UIWindow *s_applicationWindow = nil;
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowNetworkHistory,
FLEXGlobalsRowSystemLog,
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowFileBrowser,
FLEXGlobalsCookies,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowMainBundle,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowCount
};
@interface FLEXGlobalsTableViewController ()
@property (nonatomic, readonly, copy) NSArray<FLEXGlobalsTableViewControllerEntry *> *entries;
@end
@implementation FLEXGlobalsTableViewController
+ (NSArray<FLEXGlobalsTableViewControllerEntry *> *)defaultGlobalEntries
{
NSMutableArray<FLEXGlobalsTableViewControllerEntry *> *defaultGlobalEntries = [NSMutableArray array];
for (FLEXGlobalsRow defaultRowIndex = 0; defaultRowIndex < FLEXGlobalsRowCount; defaultRowIndex++) {
FLEXGlobalsTableViewControllerEntryNameFuture titleFuture = nil;
FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture = nil;
switch (defaultRowIndex) {
case FLEXGlobalsRowAppClasses:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
};
viewControllerFuture = ^UIViewController *{
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
classesViewController.binaryImageName = [FLEXUtility applicationImageName];
return classesViewController;
};
break;
case FLEXGlobalsRowSystemLibraries: {
NSString *titleString = @"📚 System Libraries";
titleFuture = ^NSString *{
return titleString;
};
viewControllerFuture = ^UIViewController *{
FLEXLibrariesTableViewController *librariesViewController = [[FLEXLibrariesTableViewController alloc] init];
librariesViewController.title = titleString;
return librariesViewController;
};
break;
}
case FLEXGlobalsRowLiveObjects:
titleFuture = ^NSString *{
return @"💩 Heap Objects";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXLiveObjectsTableViewController alloc] init];
};
break;
case FLEXGlobalsRowAppDelegate:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"👉 %@", [[[UIApplication sharedApplication] delegate] class]];
};
viewControllerFuture = ^UIViewController *{
id <UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:appDelegate];
};
break;
case FLEXGlobalsRowRootViewController:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"🌴 %@", [[s_applicationWindow rootViewController] class]];
};
viewControllerFuture = ^UIViewController *{
UIViewController *rootViewController = [s_applicationWindow rootViewController];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:rootViewController];
};
break;
case FLEXGlobalsRowUserDefaults:
titleFuture = ^NSString *{
return @"🚶 +[NSUserDefaults standardUserDefaults]";
};
viewControllerFuture = ^UIViewController *{
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:standardUserDefaults];
};
break;
case FLEXGlobalsRowMainBundle:
titleFuture = ^NSString *{
return @"📦 +[NSBundle mainBundle]";
};
viewControllerFuture = ^UIViewController *{
NSBundle *mainBundle = [NSBundle mainBundle];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:mainBundle];
};
break;
case FLEXGlobalsRowApplication:
titleFuture = ^NSString *{
return @"💾 +[UIApplication sharedApplication]";
};
viewControllerFuture = ^UIViewController *{
UIApplication *sharedApplication = [UIApplication sharedApplication];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:sharedApplication];
};
break;
case FLEXGlobalsRowKeyWindow:
titleFuture = ^NSString *{
return @"🔑 -[UIApplication keyWindow]";
};
viewControllerFuture = ^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
};
break;
case FLEXGlobalsRowMainScreen:
titleFuture = ^NSString *{
return @"💻 +[UIScreen mainScreen]";
};
viewControllerFuture = ^UIViewController *{
UIScreen *mainScreen = [UIScreen mainScreen];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:mainScreen];
};
break;
case FLEXGlobalsRowCurrentDevice:
titleFuture = ^NSString *{
return @"📱 +[UIDevice currentDevice]";
};
viewControllerFuture = ^UIViewController *{
UIDevice *currentDevice = [UIDevice currentDevice];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:currentDevice];
};
break;
case FLEXGlobalsCookies:
titleFuture = ^NSString *{
return @"🍪 Cookies";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXCookiesTableViewController alloc] init];
};
break;
case FLEXGlobalsRowFileBrowser:
titleFuture = ^NSString *{
return @"📁 File Browser";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXFileBrowserTableViewController alloc] init];
};
break;
case FLEXGlobalsRowSystemLog:
titleFuture = ^{
return @"⚠️ System Log";
};
viewControllerFuture = ^{
return [[FLEXSystemLogTableViewController alloc] init];
};
break;
case FLEXGlobalsRowNetworkHistory:
titleFuture = ^{
return @"📡 Network History";
};
viewControllerFuture = ^{
return [[FLEXNetworkHistoryTableViewController alloc] init];
};
break;
case FLEXGlobalsRowCount:
break;
}
NSParameterAssert(titleFuture);
NSParameterAssert(viewControllerFuture);
[defaultGlobalEntries addObject:[FLEXGlobalsTableViewControllerEntry entryWithNameFuture:titleFuture viewControllerFuture:viewControllerFuture]];
}
return defaultGlobalEntries;
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
self.title = @"💪 FLEX";
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
}
return self;
}
#pragma mark - Public
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
{
s_applicationWindow = applicationWindow;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
}
#pragma mark -
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsTableViewControllerEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
{
return self.entries[indexPath.row];
}
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.entryNameFuture();
}
- (UIViewController *)viewControllerToPushForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.viewControllerFuture();
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.entries count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIViewController *viewControllerToPush = [self viewControllerToPushForRowAtIndexPath:indexPath];
[self.navigationController pushViewController:viewControllerToPush animated:YES];
}
@end
@@ -57,11 +57,12 @@
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className
{
const char *classNameCString = [className UTF8String];
const char *classNameCString = className.UTF8String;
NSMutableArray *instances = [NSMutableArray array];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
// Note: objects of certain classes crash when retain is called. It is up to the user to avoid tapping into instance lists for these classes.
// Note: objects of certain classes crash when retain is called.
// It is up to the user to avoid tapping into instance lists for these classes.
// Ex. OS_dispatch_queue_specific_queue
// In the future, we could provide some kind of warning for classes that are known to be problematic.
if (malloc_size((__bridge const void *)(object)) > 0) {
@@ -71,7 +72,7 @@
}];
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
FLEXInstancesTableViewController *viewController = [[self alloc] initWithReferences:references];
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)[instances count]];
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
return viewController;
}
@@ -93,7 +94,7 @@
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
Ivar ivar = ivars[ivarIndex];
const char *typeEncoding = ivar_getTypeEncoding(ivar);
if (typeEncoding[0] == @encode(id)[0] || typeEncoding[0] == @encode(Class)[0]) {
if (typeEncoding[0] == FLEXTypeEncodingObjcObject || typeEncoding[0] == FLEXTypeEncodingObjcClass) {
ptrdiff_t offset = ivar_getOffset(ivar);
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
@@ -209,7 +210,7 @@
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = cellFont;
cell.detailTextLabel.font = cellFont;
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.detailTextLabel.textColor = UIColor.grayColor;
}
FLEXObjectRef *row = self.sections[indexPath.section][indexPath.row];
@@ -6,8 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLibrariesTableViewController : UITableViewController
@interface FLEXLibrariesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -12,13 +12,13 @@
#import "FLEXClassExplorerViewController.h"
#import <objc/runtime.h>
@interface FLEXLibrariesTableViewController () <UISearchBarDelegate>
@interface FLEXLibrariesTableViewController ()
@property (nonatomic, strong) NSArray<NSString *> *imageNames;
@property (nonatomic, strong) NSArray<NSString *> *filteredImageNames;
@property (nonatomic) NSArray<NSString *> *imageNames;
@property (nonatomic) NSArray<NSString *> *filteredImageNames;
@property (nonatomic) NSString *headerTitle;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic, strong) Class foundClass;
@property (nonatomic) Class foundClass;
@end
@@ -37,11 +37,37 @@
{
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.delegate = self;
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
self.showsSearchBar = YES;
[self updateHeaderTitle];
}
- (void)updateHeaderTitle
{
if (self.foundClass) {
self.headerTitle = @"Looking for this?";
} else if (self.imageNames.count == self.filteredImageNames.count) {
// Unfiltered
self.headerTitle = [NSString stringWithFormat:@"%@ libraries", @(self.imageNames.count)];
} else {
self.headerTitle = [NSString
stringWithFormat:@"%@ of %@ libraries",
@(self.filteredImageNames.count), @(self.imageNames.count)
];
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"📚 System Libraries";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLibrariesTableViewController *librariesViewController = [self new];
librariesViewController.title = [self globalsEntryTitle:row];
return librariesViewController;
}
@@ -94,10 +120,10 @@
#pragma mark - Filtering
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
- (void)updateSearchResults:(NSString *)searchText
{
if ([searchText length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
if (searchText.length) {
NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
BOOL matches = NO;
NSString *shortName = [self shortNameForImageName:evaluatedObject];
if ([shortName rangeOfString:searchText options:NSCaseInsensitiveSearch].length > 0) {
@@ -105,28 +131,19 @@
}
return matches;
}];
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPreidcate];
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredImageNames = self.imageNames;
}
self.foundClass = NSClassFromString(searchText);
[self updateHeaderTitle];
[self.tableView reloadData];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.filteredImageNames.count + (self.foundClass ? 1 : 0);
@@ -145,7 +162,7 @@
NSString *executablePath;
if (self.foundClass) {
if (indexPath.row == 0) {
cell.textLabel.text = [NSString stringWithFormat:@"Class \"%@\"", self.searchBar.text];
cell.textLabel.text = [NSString stringWithFormat:@"Class \"%@\"", self.searchText];
return cell;
} else {
executablePath = self.filteredImageNames[indexPath.row-1];
@@ -158,6 +175,11 @@
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
#pragma mark - Table View Delegate
@@ -168,7 +190,7 @@
objectExplorer.object = self.foundClass;
[self.navigationController pushViewController:objectExplorer animated:YES];
} else {
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
FLEXClassesTableViewController *classesViewController = [FLEXClassesTableViewController new];
classesViewController.binaryImageName = self.filteredImageNames[self.foundClass ? indexPath.row-1 : indexPath.row];
[self.navigationController pushViewController:classesViewController animated:YES];
}
@@ -6,8 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLiveObjectsTableViewController : UITableViewController
@interface FLEXLiveObjectsTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -10,37 +10,42 @@
#import "FLEXHeapEnumerator.h"
#import "FLEXInstancesTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import <objc/runtime.h>
static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
@interface FLEXLiveObjectsTableViewController () <UISearchBarDelegate>
@interface FLEXLiveObjectsTableViewController ()
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
@property (nonatomic, readonly) NSArray<NSString *> *allClassNames;
@property (nonatomic, strong) NSArray<NSString *> *filteredClassNames;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic) NSArray<NSString *> *filteredClassNames;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXLiveObjectsTableViewController
- (void)loadView
{
self.tableView = [FLEXTableView flexDefaultTableView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// self.title = @"Live Objects";
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
self.searchBar.delegate = self;
self.searchBar.showsScopeBar = YES;
self.searchBar.scopeButtonTitles = @[@"Sort Alphabetically", @"Sort by Count", @"Sort by Size"];
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
self.refreshControl = [[UIRefreshControl alloc] init];
self.refreshControl = [UIRefreshControl new];
[self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
[self reloadTableData];
@@ -48,7 +53,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
- (NSArray<NSString *> *)allClassNames
{
return [self.instanceCountsForClassNames allKeys];
return self.instanceCountsForClassNames.allKeys;
}
- (void)reloadTableData
@@ -90,7 +95,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
self.instanceCountsForClassNames = mutableCountsForClassNames;
self.instanceSizesForClassNames = mutableSizesForClassNames;
[self updateTableDataForSearchFilter];
[self updateSearchResults:nil];
}
- (void)refreshControlDidRefresh:(id)sender
@@ -99,80 +104,82 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
[self.refreshControl endRefreshing];
}
- (void)updateTitle
- (void)updateHeaderTitle
{
NSString *title = @"Live Objects";
NSUInteger totalCount = 0;
NSUInteger totalSize = 0;
for (NSString *className in self.allClassNames) {
NSUInteger count = [self.instanceCountsForClassNames[className] unsignedIntegerValue];
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
totalCount += count;
totalSize += count * [self.instanceSizesForClassNames[className] unsignedIntegerValue];
totalSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
}
NSUInteger filteredCount = 0;
NSUInteger filteredSize = 0;
for (NSString *className in self.filteredClassNames) {
NSUInteger count = [self.instanceCountsForClassNames[className] unsignedIntegerValue];
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
filteredCount += count;
filteredSize += count * [self.instanceSizesForClassNames[className] unsignedIntegerValue];
filteredSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
}
if (filteredCount == totalCount) {
// Unfiltered
title = [title stringByAppendingFormat:@" (%lu, %@)", (unsigned long)totalCount,
[NSByteCountFormatter stringFromByteCount:totalSize countStyle:NSByteCountFormatterCountStyleFile]];
self.headerTitle = [NSString
stringWithFormat:@"%@ objects, %@",
@(totalCount), [NSByteCountFormatter
stringFromByteCount:totalSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
} else {
title = [title stringByAppendingFormat:@" (filtered, %lu, %@)", (unsigned long)filteredCount,
[NSByteCountFormatter stringFromByteCount:filteredSize countStyle:NSByteCountFormatterCountStyleFile]];
self.headerTitle = [NSString
stringWithFormat:@"%@ of %@ objects, %@",
@(filteredCount), @(totalCount), [NSByteCountFormatter
stringFromByteCount:filteredSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"💩 Heap Objects";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLiveObjectsTableViewController *liveObjectsViewController = [self new];
liveObjectsViewController.title = [self globalsEntryTitle:row];
return liveObjectsViewController;
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)filter
{
NSInteger selectedScope = self.selectedScope;
self.title = title;
}
#pragma mark - Search
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[self updateTableDataForSearchFilter];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
{
[self updateTableDataForSearchFilter];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Dismiss the keyboard when interacting with filtered results.
[self.searchBar endEditing:YES];
}
- (void)updateTableDataForSearchFilter
{
if ([self.searchBar.text length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", self.searchBar.text];
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPreidcate];
if (filter.length) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", filter];
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredClassNames = self.allClassNames;
}
if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortAlphabeticallyIndex) {
if (selectedScope == kFLEXLiveObjectsSortAlphabeticallyIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortByCountIndex) {
} else if (selectedScope == kFLEXLiveObjectsSortByCountIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
// Reversed for descending counts.
return [count2 compare:count1];
}];
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortBySizeIndex) {
} else if (selectedScope == kFLEXLiveObjectsSortBySizeIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
@@ -183,7 +190,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
}];
}
[self updateTitle];
[self updateHeaderTitle];
[self.tableView reloadData];
}
@@ -197,29 +204,37 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.filteredClassNames count];
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (UITableViewCell *)tableView:(__kindof UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
}
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:[tableView defaultReuseIdentifier]
forIndexPath:indexPath
];
NSString *className = self.filteredClassNames[indexPath.row];
NSNumber *count = self.instanceCountsForClassNames[className];
NSNumber *size = self.instanceSizesForClassNames[className];
unsigned long totalSize = count.unsignedIntegerValue * size.unsignedIntegerValue;
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)", className, (long)[count integerValue],
[NSByteCountFormatter stringFromByteCount:totalSize countStyle:NSByteCountFormatterCountStyleFile]];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)",
className, (long)[count integerValue],
[NSByteCountFormatter
stringFromByteCount:totalSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
#pragma mark - Table view delegate
@@ -8,11 +8,12 @@
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import <WebKit/WebKit.h>
@interface FLEXWebViewController () <UIWebViewDelegate>
@interface FLEXWebViewController () <WKNavigationDelegate>
@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) NSString *originalText;
@property (nonatomic) WKWebView *webView;
@property (nonatomic) NSString *originalText;
@end
@@ -22,10 +23,14 @@
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.webView = [[UIWebView alloc] init];
self.webView.delegate = self;
self.webView.dataDetectorTypes = UIDataDetectorTypeLink;
self.webView.scalesPageToFit = YES;
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
if (@available(iOS 10.0, *)) {
configuration.dataDetectorTypes = UIDataDetectorTypeLink;
}
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
self.webView.navigationDelegate = self;
}
return self;
}
@@ -53,9 +58,9 @@
- (void)dealloc
{
// UIWebView's delegate is assign so we need to clear it manually.
if (_webView.delegate == self) {
_webView.delegate = nil;
// WKWebView's delegate is assigned so we need to clear it manually.
if (_webView.navigationDelegate == self) {
_webView.navigationDelegate = nil;
}
}
@@ -67,33 +72,34 @@
self.webView.frame = self.view.bounds;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if ([self.originalText length] > 0) {
if (self.originalText.length > 0) {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)];
}
}
- (void)copyButtonTapped:(id)sender
{
[[UIPasteboard generalPasteboard] setString:self.originalText];
[UIPasteboard.generalPasteboard setString:self.originalText];
}
#pragma mark - UIWebView Delegate
#pragma mark - WKWebView Delegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
BOOL shouldStart = NO;
if (navigationType == UIWebViewNavigationTypeOther) {
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
if (navigationAction.navigationType == WKNavigationTypeOther) {
// Allow the initial load
shouldStart = YES;
policy = WKNavigationActionPolicyAllow;
} else {
// For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
// Don't allow the current web view do handle the navigation.
// Don't allow the current web view to handle the navigation.
NSURLRequest *request = navigationAction.request;
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
webVC.title = [[request URL] absoluteString];
[self.navigationController pushViewController:webVC animated:YES];
}
return shouldStart;
decisionHandler(policy);
}
@@ -111,17 +117,17 @@
+ (NSSet<NSString *> *)webViewSupportedPathExtensions
{
static NSSet<NSString *> *pathExtenstions = nil;
static NSSet<NSString *> *pathExtensions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Note that this is not exhaustive, but all these extensions should work well in the web view.
// See https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
pathExtenstions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
// See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
pathExtensions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
});
return pathExtenstions;
return pathExtensions;
}
@end
@@ -1,6 +1,6 @@
//
// FLEXFileBrowserSearchOperation.h
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/8/4.
// Copyright (c) 2014年 f. All rights reserved.
@@ -1,6 +1,6 @@
//
// FLEXFileBrowserSearchOperation.m
// UICatalog
// FLEX
//
// Created by on 2014/8/4.
// Copyright (c) 2014 f. All rights reserved.
@@ -17,7 +17,7 @@
- (id)flex_pop
{
id anObject = [self lastObject];
id anObject = self.lastObject;
[self removeLastObject];
return anObject;
}
@@ -26,8 +26,8 @@
@interface FLEXFileBrowserSearchOperation ()
@property (nonatomic, strong) NSString *path;
@property (nonatomic, strong) NSString *searchString;
@property (nonatomic) NSString *path;
@property (nonatomic) NSString *searchString;
@end
@@ -37,7 +37,7 @@
- (uint64_t)totalSizeAtPath:(NSString *)path
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
@@ -64,7 +64,7 @@
- (void)main
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSFileManager *fileManager = NSFileManager.defaultManager;
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary dictionary];
uint64_t totalSize = 0;
@@ -72,7 +72,7 @@
[stack flex_push:self.path];
//recursive found all match searchString paths, and precomputing there size
while ([stack count]) {
while (stack.count) {
NSString *currentPath = [stack flex_pop];
NSArray<NSString *> *directoryPath = [fileManager contentsOfDirectoryAtPath:currentPath error:nil];
@@ -6,11 +6,11 @@
// Based on previous work by Evan Doll
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserTableViewController : UITableViewController
@interface FLEXFileBrowserTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
- (id)initWithPath:(NSString *)path;
@@ -0,0 +1,501 @@
//
// FLEXFileBrowserTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXTableListViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserSearchOperationDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
@property (nonatomic) NSArray<NSString *> *searchPaths;
@property (nonatomic) NSNumber *recursiveSize;
@property (nonatomic) NSNumber *searchPathsSize;
@property (nonatomic) NSOperationQueue *operationQueue;
@property (nonatomic) UIDocumentInteractionController *documentController;
@end
@implementation FLEXFileBrowserTableViewController
- (id)init
{
return [self initWithPath:NSHomeDirectory()];
}
- (id)initWithPath:(NSString *)path
{
self = [super init];
if (self) {
self.path = path;
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
//computing path size
FLEXFileBrowserTableViewController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away.
if (!weakSelf) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
});
[self reloadCurrentPath];
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceForAsyncSearch;
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowBrowseBundle: return @"📁 Browse Bundle Directory";
case FLEXGlobalsRowBrowseContainer: return @"📁 Browse Container Directory";
default: return nil;
}
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowBrowseBundle: return [[self alloc] initWithPath:NSBundle.mainBundle.bundlePath];
case FLEXGlobalsRowBrowseContainer: return [[self alloc] initWithPath:NSHomeDirectory()];
default: return [self new];
}
}
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size
{
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.tableView reloadData];
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)newText
{
[self reloadDisplayedPaths];
}
#pragma mark UISearchControllerDelegate
- (void)willDismissSearchController:(UISearchController *)searchController
{
[self.operationQueue cancelAllOperations];
[self reloadCurrentPath];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.searchController.isActive ? self.searchPaths.count : self.childPaths.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
BOOL isSearchActive = self.searchController.isActive;
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
NSArray<NSString *> *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
NSString *sizeString = nil;
if (!currentSize) {
sizeString = @"Computing size…";
} else {
sizeString = [NSByteCountFormatter stringFromByteCount:[currentSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
}
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)currentPaths.count, sizeString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSDictionary<NSString *, id> *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:NULL];
BOOL isDirectory = [attributes.fileType isEqual:NSFileTypeDirectory];
NSString *subtitle = nil;
if (isDirectory) {
NSUInteger count = [NSFileManager.defaultManager contentsOfDirectoryAtPath:fullPath error:NULL].count;
subtitle = [NSString stringWithFormat:@"%lu item%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
} else {
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:attributes.fileSize countStyle:NSByteCountFormatterCountStyleFile];
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, attributes.fileModificationDate ?: @"Never modified"];
}
static NSString *textCellIdentifier = @"textCell";
static NSString *imageCellIdentifier = @"imageCell";
UITableViewCell *cell = nil;
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
NSString *cellIdentifier = image ? imageCellIdentifier : textCellIdentifier;
if (!cell) {
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = UIColor.grayColor;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSString *cellTitle = [fullPath lastPathComponent];
cell.textLabel.text = cellTitle;
cell.detailTextLabel.text = subtitle;
if (image) {
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.imageView.image = image;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSString *subpath = fullPath.lastPathComponent;
NSString *pathExtension = subpath.pathExtension;
BOOL isDirectory = NO;
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIImage *image = cell.imageView.image;
if (!stillExists) {
[FLEXAlert showAlert:@"File Not Found" message:@"The file at the specified path no longer exists." from:self];
[self reloadDisplayedPaths];
return;
}
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if (image) {
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
if (!fileData.length) {
[FLEXAlert showAlert:@"Empty File" message:@"No data returned from the file." from:self];
return;
}
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([pathExtension isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:fileData];
} else {
// Regardless of file extension...
id object = nil;
@try {
// Try to decode an archived object regardless of file extension
object = [NSKeyedUnarchiver unarchiveObjectWithData:fileData];
} @catch (NSException *e) { }
// Try to decode other things instead
object = object
?: [NSPropertyListSerialization propertyListWithData:fileData
options:0
format:NULL
error:NULL]
?: [NSDictionary dictionaryWithContentsOfFile:fullPath]
?: [NSArray arrayWithContentsOfFile:fullPath];
if (object) {
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
}
}
if (prettyString.length) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else if ([FLEXTableListViewController supportsExtension:pathExtension]) {
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
}
else if (!drillInViewController) {
NSString *fileString = [NSString stringWithUTF8String:fileData.bytes];
if (fileString.length) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
}
}
}
if (drillInViewController) {
drillInViewController.title = subpath.lastPathComponent;
[self.navigationController pushViewController:drillInViewController animated:YES];
} else {
// Share the file otherwise
[self openFileController:fullPath];
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIMenuItem *rename = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *delete = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
UIMenuItem *copyPath = [[UIMenuItem alloc] initWithTitle:@"Copy Path" action:@selector(fileBrowserCopyPath:)];
UIMenuItem *share = [[UIMenuItem alloc] initWithTitle:@"Share" action:@selector(fileBrowserShare:)];
UIMenuController.sharedMenuController.menuItems = @[rename, delete, copyPath, share];
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(fileBrowserDelete:)
|| action == @selector(fileBrowserRename:)
|| action == @selector(fileBrowserCopyPath:)
|| action == @selector(fileBrowserShare:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
// Empty, but has to exist for the menu to show
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0)
{
__weak typeof(self) weakSelf = self;
return [UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
UIAction *rename = [UIAction actionWithTitle:@"Rename"
image:nil
identifier:@"Rename"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserRename:cell];
}];
UIAction *delete = [UIAction actionWithTitle:@"Delete"
image:nil
identifier:@"Delete"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserDelete:cell];
}];
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path"
image:nil
identifier:@"Copy Path"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserCopyPath:cell];
}];
UIAction *share = [UIAction actionWithTitle:@"Share"
image:nil
identifier:@"Share"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserShare:cell];
}];
return [UIMenu menuWithTitle:@"Manage File" image:nil identifier:@"Manage File" options:UIMenuOptionsDisplayInline children:@[rename, delete, copyPath, share]];
}];
}
#endif
- (void)openFileController:(NSString *)fullPath
{
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [NSURL fileURLWithPath:fullPath];
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
self.documentController = controller;
}
- (void)fileBrowserRename:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
if (stillExists) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([NSString stringWithFormat:@"Rename %@?", fullPath.lastPathComponent]);
make.configuredTextField(^(UITextField *textField) {
textField.placeholder = @"New file name";
textField.text = fullPath.lastPathComponent;
});
make.button(@"Rename").handler(^(NSArray<NSString *> *strings) {
NSString *newFileName = strings.firstObject;
NSString *newPath = [fullPath.stringByDeletingLastPathComponent stringByAppendingPathComponent:newFileName];
[NSFileManager.defaultManager moveItemAtPath:fullPath toPath:newPath error:NULL];
[self reloadDisplayedPaths];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
}
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
BOOL isDirectory = NO;
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Confirm Deletion");
make.message([NSString stringWithFormat:
@"The %@ '%@' will be deleted. This operation cannot be undone",
(isDirectory ? @"directory" : @"file"), fullPath.lastPathComponent
]);
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[NSFileManager.defaultManager removeItemAtPath:fullPath error:NULL];
[self reloadDisplayedPaths];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
}
}
- (void)fileBrowserCopyPath:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
UIPasteboard.generalPasteboard.string = fullPath;
}
- (void)fileBrowserShare:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *pathString = [self filePathAtIndexPath:indexPath];
NSURL *filePath = [NSURL fileURLWithPath:pathString];
BOOL isDirectory = NO;
[NSFileManager.defaultManager fileExistsAtPath:pathString isDirectory:&isDirectory];
if (isDirectory) {
// UIDocumentInteractionController for folders
[self openFileController:pathString];
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
[self presentViewController:shareSheet animated:true completion:nil];
}
}
- (void)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self updateSearchPaths];
} else {
[self reloadCurrentPath];
[self.tableView reloadData];
}
}
- (void)reloadCurrentPath
{
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
NSArray<NSString *> *subpaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:NULL];
for (NSString *subpath in subpaths) {
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
}
self.childPaths = childPaths;
}
- (void)updateSearchPaths
{
self.searchPaths = nil;
self.searchPathsSize = nil;
//clear pre search request and start a new one
[self.operationQueue cancelAllOperations];
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchText];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
}
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
{
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
}
@end
@implementation FLEXFileBrowserTableViewCell
- (void)forwardAction:(SEL)action withSender:(id)sender
{
id target = [self.nextResponder targetForAction:action withSender:sender];
[UIApplication.sharedApplication sendAction:action to:target from:self forEvent:nil];
}
- (void)fileBrowserRename:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserDelete:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserCopyPath:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserShare:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
@end
@@ -0,0 +1,81 @@
//
// FLEXGlobalsEntry.h
// FLEX
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewSection.h"
@class FLEXGlobalsTableViewController;
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowProcessInfo,
FLEXGlobalsRowNetworkHistory,
FLEXGlobalsRowSystemLog,
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowAddressInspector,
FLEXGlobalsRowCookies,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowAppKeychainItems,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowMainBundle,
FLEXGlobalsRowBrowseBundle,
FLEXGlobalsRowBrowseContainer,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowPasteboard,
FLEXGlobalsRowCount
};
typedef NSString *(^FLEXGlobalsEntryNameFuture)(void);
/// Simply return a view controller to be pushed on the navigation stack
typedef UIViewController *(^FLEXGlobalsTableViewControllerViewControllerFuture)(void);
/// Do something like present an alert, then use the host
/// view controller to present or push another view controller.
typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsTableViewController *host);
/// For view controllers to conform to to indicate they support being used
/// in the globals table view controller. These methods help create concrete entries.
///
/// Previously, the concrete entries relied on "futures" for the view controller and title.
/// With this protocol, the conforming class itself can act as a future, since the methods
/// will not be invoked until the title and view controller / row action are needed.
@protocol FLEXGlobalsEntry <NSObject>
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row;
// Must respond to at least one of the below
@optional
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row;
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row;
@end
@interface FLEXGlobalsEntry : NSObject <FLEXPatternMatching>
@property (nonatomic, readonly) FLEXGlobalsEntryNameFuture entryNameFuture;
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture;
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerRowAction rowAction;
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)entry row:(FLEXGlobalsRow)row;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction;
@end
@interface NSObject (FLEXGlobalsEntry)
/// @return The result of passing self to +[FLEXGlobalsEntry entryWithEntry:]
/// if the class conforms to FLEXGlobalsEntry, else, nil.
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row;
@end
@@ -0,0 +1,80 @@
//
// FLEXGlobalsEntry.m
// FLEX
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
@implementation FLEXGlobalsEntry
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)cls row:(FLEXGlobalsRow)row
{
NSParameterAssert(cls);
NSParameterAssert(
[cls respondsToSelector:@selector(globalsEntryViewController:)] ||
[cls respondsToSelector:@selector(globalsEntryRowAction:)]
);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = ^{ return [cls globalsEntryTitle:row]; };
if ([cls respondsToSelector:@selector(globalsEntryViewController:)]) {
entry->_viewControllerFuture = ^{ return [cls globalsEntryViewController:row]; };
} else {
entry->_rowAction = [cls globalsEntryRowAction:row];
}
return entry;
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture
{
NSParameterAssert(nameFuture);
NSParameterAssert(viewControllerFuture);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = [nameFuture copy];
entry->_viewControllerFuture = [viewControllerFuture copy];
return entry;
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction
{
NSParameterAssert(nameFuture);
NSParameterAssert(rowSelectedAction);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = [nameFuture copy];
entry->_rowAction = [rowSelectedAction copy];
return entry;
}
#pragma mark FLEXPatternMatching
- (BOOL)matches:(NSString *)query
{
return [self.entryNameFuture() localizedCaseInsensitiveContainsString:query];
}
@end
#pragma mark - flex_concreteGlobalsEntry
@implementation NSObject (FLEXGlobalsEntry)
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row {
if ([self conformsToProtocol:@protocol(FLEXGlobalsEntry)]) {
return [FLEXGlobalsEntry entryWithEntry:self row:row];
}
return nil;
}
@end
@@ -6,11 +6,24 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
@protocol FLEXGlobalsTableViewControllerDelegate;
@interface FLEXGlobalsTableViewController : UITableViewController
typedef NS_ENUM(NSUInteger, FLEXGlobalsSection) {
/// NSProcessInfo, Network history, system log,
/// heap, address explorer, libraries, app classes
FLEXGlobalsSectionProcessAndEvents,
/// Browse container, browse bundle, NSBundle.main,
/// NSUserDefaults.standard, UIApplication,
/// app delegate, key window, root VC, cookies
FLEXGlobalsSectionAppShortcuts,
/// UIPasteBoard.general, UIScreen, UIDevice
FLEXGlobalsSectionMisc,
FLEXGlobalsSectionCustom,
FLEXGlobalsSectionCount
};
@interface FLEXGlobalsTableViewController : FLEXTableViewController
@property (nonatomic, weak) id <FLEXGlobalsTableViewControllerDelegate> delegate;
@@ -0,0 +1,292 @@
//
// FLEXGlobalsTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXGlobalsTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXCookiesTableViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXTableViewSection.h"
static __weak UIWindow *s_applicationWindow = nil;
@interface FLEXGlobalsTableViewController ()
@property (nonatomic, readonly) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections;
@property (nonatomic, copy) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *filteredSections;
@end
@implementation FLEXGlobalsTableViewController
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSection)section
{
switch (section) {
case FLEXGlobalsSectionProcessAndEvents:
return @"Process and Events";
case FLEXGlobalsSectionAppShortcuts:
return @"App Shortcuts";
case FLEXGlobalsSectionMisc:
return @"Miscellaneous";
case FLEXGlobalsSectionCustom:
return @"Custom Additions";
default:
@throw NSInternalInconsistencyException;
}
}
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row
{
switch (row) {
case FLEXGlobalsRowAppClasses:
return [FLEXClassesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAppKeychainItems:
return [FLEXKeychainTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAddressInspector:
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLibraries:
return [FLEXLibrariesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowLiveObjects:
return [FLEXLiveObjectsTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowCookies:
return [FLEXCookiesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseBundle:
case FLEXGlobalsRowBrowseContainer:
return [FLEXFileBrowserTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLog:
return [FLEXSystemLogTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowNetworkHistory:
return [FLEXNetworkHistoryTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowKeyWindow:
return [FLEXGlobalsEntry
entryWithNameFuture:^NSString *{
return @"🔑 -[UIApplication keyWindow]";
} viewControllerFuture:^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
}
];
case FLEXGlobalsRowRootViewController:
case FLEXGlobalsRowProcessInfo:
case FLEXGlobalsRowAppDelegate:
case FLEXGlobalsRowUserDefaults:
case FLEXGlobalsRowMainBundle:
case FLEXGlobalsRowApplication:
case FLEXGlobalsRowMainScreen:
case FLEXGlobalsRowCurrentDevice:
case FLEXGlobalsRowPasteboard:
return [FLEXObjectExplorerFactory flex_concreteGlobalsEntry:row];
default:
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:@"Missing globals case in switch" userInfo:nil
];
}
}
+ (NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *)defaultGlobalSections
{
static NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *rows = @[
@[
[self globalsEntryForRow:FLEXGlobalsRowNetworkHistory],
[self globalsEntryForRow:FLEXGlobalsRowSystemLog],
[self globalsEntryForRow:FLEXGlobalsRowProcessInfo],
[self globalsEntryForRow:FLEXGlobalsRowLiveObjects],
[self globalsEntryForRow:FLEXGlobalsRowAddressInspector],
[self globalsEntryForRow:FLEXGlobalsRowSystemLibraries],
[self globalsEntryForRow:FLEXGlobalsRowAppClasses],
],
@[ // FLEXGlobalsSectionAppShortcuts
[self globalsEntryForRow:FLEXGlobalsRowBrowseBundle],
[self globalsEntryForRow:FLEXGlobalsRowBrowseContainer],
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
[self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
[self globalsEntryForRow:FLEXGlobalsRowAppKeychainItems],
[self globalsEntryForRow:FLEXGlobalsRowApplication],
[self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
[self globalsEntryForRow:FLEXGlobalsRowKeyWindow],
[self globalsEntryForRow:FLEXGlobalsRowRootViewController],
[self globalsEntryForRow:FLEXGlobalsRowCookies],
],
@[ // FLEXGlobalsSectionMisc
[self globalsEntryForRow:FLEXGlobalsRowPasteboard],
[self globalsEntryForRow:FLEXGlobalsRowMainScreen],
[self globalsEntryForRow:FLEXGlobalsRowCurrentDevice],
]
];
NSMutableArray *tmp = [NSMutableArray array];
for (NSInteger i = 0; i < FLEXGlobalsSectionCount - 1; i++) { // Skip custom
NSString *title = [self globalsTitleForSection:i];
[tmp addObject:[FLEXTableViewSection section:i title:title rows:rows[i]]];
}
sections = tmp.copy;
});
return sections;
}
#pragma mark - Public
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
{
s_applicationWindow = applicationWindow;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = @"💪 FLEX";
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
// Table view data
_sections = [[self class] defaultGlobalSections];
if ([FLEXManager sharedManager].userGlobalEntries.count) {
// Make custom section
NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
FLEXTableViewSection *custom = [FLEXTableViewSection
section:FLEXGlobalsSectionCustom
title:title
rows:[FLEXManager sharedManager].userGlobalEntries
];
_sections = [_sections arrayByAddingObject:custom];
}
// Done button
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(donePressed:)
];
}
#pragma mark - Search Bar
- (void)updateSearchResults:(NSString *)newText {
if (!newText.length) {
self.filteredSections = nil;
[self.tableView reloadData];
return;
}
// Sections are a map of index to rows, since empty sections are omitted
NSMutableArray *filteredSections = [NSMutableArray array];
[self.sections enumerateObjectsUsingBlock:^(FLEXTableViewSection<FLEXGlobalsEntry *> *section, NSUInteger idx, BOOL *stop) {
section = [section newSectionWithRowsMatchingQuery:newText];
if (section) {
[filteredSections addObject:section];
}
}];
self.filteredSections = filteredSections.copy;
[self.tableView reloadData];
}
#pragma mark - Misc
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
{
if (self.filteredSections) {
return self.filteredSections[indexPath.section][indexPath.row];
} else {
return self.sections[indexPath.section][indexPath.row];
}
}
- (NSString *)titleForSection:(NSInteger)section
{
if (self.filteredSections) {
return self.filteredSections[section].title;
} else {
return self.sections[section].title;
}
}
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.entryNameFuture();
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.filteredSections ? self.filteredSections.count : self.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (self.filteredSections) {
return self.filteredSections[section].count;
} else {
return self.sections[section].count;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [self titleForSection:section];
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
if (entry.viewControllerFuture) {
[self.navigationController pushViewController:entry.viewControllerFuture() animated:YES];
} else {
entry.rowAction(self);
}
}
@end
@@ -0,0 +1,141 @@
//
// FLEXKeychain.h
//
// Derived from:
// SSKeychain.h in SSKeychain
// Created by Sam Soffes on 5/19/10.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import <Foundation/Foundation.h>
/// Error code specific to FLEXKeychain that can be returned in NSError objects.
/// For codes returned by the operating system, refer to SecBase.h for your
/// platform.
typedef NS_ENUM(OSStatus, FLEXKeychainErrorCode) {
/// Some of the arguments were invalid.
FLEXKeychainErrorBadArguments = -1001,
};
/// FLEXKeychain error domain
extern NSString *const kFLEXKeychainErrorDomain;
/// Account name.
extern NSString *const kFLEXKeychainAccountKey;
/// Time the item was created.
///
/// The value will be a string.
extern NSString *const kFLEXKeychainCreatedAtKey;
/// Item class.
extern NSString *const kFLEXKeychainClassKey;
/// Item description.
extern NSString *const kFLEXKeychainDescriptionKey;
/// Item label.
extern NSString *const kFLEXKeychainLabelKey;
/// Time the item was last modified.
///
/// The value will be a string.
extern NSString *const kFLEXKeychainLastModifiedKey;
/// Where the item was created.
extern NSString *const kFLEXKeychainWhereKey;
/// A simple wrapper for accessing accounts, getting passwords,
/// setting passwords, and deleting passwords using the system Keychain.
@interface FLEXKeychain : NSObject
#pragma mark - Classic methods
/// @param serviceName The service for which to return the corresponding password.
/// @param account The account for which to return the corresponding password.
/// @return Returns a string containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Returns a nsdata containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
///
/// @param serviceName The service for which to return the corresponding password.
/// @param account The account for which to return the corresponding password.
/// @return Returns a nsdata containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Deletes a password from the Keychain.
///
/// @param serviceName The service for which to delete the corresponding password.
/// @param account The account for which to delete the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Sets a password in the Keychain.
///
/// @param password The password to store in the Keychain.
/// @param serviceName The service for which to set the corresponding password.
/// @param account The account for which to set the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Sets a password in the Keychain.
///
/// @param password The password to store in the Keychain.
/// @param serviceName The service for which to set the corresponding password.
/// @param account The account for which to set the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// @return An array of dictionaries containing the Keychain's accounts, or `nil` if
/// the Keychain doesn't have any accounts. The order of the objects in the array isn't defined.
///
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
/// can be used when accessing the dictionaries returned by this method.
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts;
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts:(NSError *__autoreleasing *)error;
/// @param serviceName The service for which to return the corresponding accounts.
/// @return An array of dictionaries containing the Keychain's accounts for a given `serviceName`,
/// or `nil` if the Keychain doesn't have any accounts for the given `serviceName`.
/// The order of the objects in the array isn't defined.
///
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
/// can be used when accessing the dictionaries returned by this method.
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName;
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error;
#pragma mark - Configuration
#if __IPHONE_4_0 && TARGET_OS_IPHONE
/// Returns the accessibility type for all future passwords saved to the Keychain.
///
/// @return `NULL` or one of the "Keychain Item Accessibility
/// Constants" used for determining when a keychain item should be readable.
+ (CFTypeRef)accessibilityType;
/// Sets the accessibility type for all future passwords saved to the Keychain.
///
/// @param accessibilityType One of the "Keychain Item Accessibility Constants"
/// used for determining when a keychain item should be readable.
/// If the value is `NULL` (the default), the Keychain default will be used which
/// is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
/// for background applications or `kSecAttrAccessibleWhenUnlocked` for all
/// other applications.
///
/// @note See Security/SecItem.h
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType;
#endif
@end
@@ -0,0 +1,120 @@
//
// FLEXKeychain.m
//
// Forked from:
// SSKeychain.m in SSKeychain
// Created by Sam Soffes on 5/19/10.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
NSString * const kFLEXKeychainErrorDomain = @"com.flipboard.flex";
NSString * const kFLEXKeychainAccountKey = @"acct";
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
NSString * const kFLEXKeychainClassKey = @"labl";
NSString * const kFLEXKeychainDescriptionKey = @"desc";
NSString * const kFLEXKeychainLabelKey = @"labl";
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
NSString * const kFLEXKeychainWhereKey = @"svce";
#if __IPHONE_4_0 && TARGET_OS_IPHONE
static CFTypeRef FLEXKeychainAccessibilityType = NULL;
#endif
@implementation FLEXKeychain
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account {
return [self passwordForService:serviceName account:account error:nil];
}
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
[query fetch:error];
return query.password;
}
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account {
return [self passwordDataForService:serviceName account:account error:nil];
}
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
[query fetch:error];
return query.passwordData;
}
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account {
return [self deletePasswordForService:serviceName account:account error:nil];
}
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
return [query deleteItem:error];
}
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account {
return [self setPassword:password forService:serviceName account:account error:nil];
}
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
query.password = password;
return [query save:error];
}
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account {
return [self setPasswordData:password forService:serviceName account:account error:nil];
}
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
query.passwordData = password;
return [query save:error];
}
+ (NSArray *)allAccounts {
return [self allAccounts:nil];
}
+ (NSArray *)allAccounts:(NSError *__autoreleasing *)error {
return [self accountsForService:nil error:error];
}
+ (NSArray *)accountsForService:(NSString *)serviceName {
return [self accountsForService:serviceName error:nil];
}
+ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
return [query fetchAll:error];
}
#if __IPHONE_4_0 && TARGET_OS_IPHONE
+ (CFTypeRef)accessibilityType {
return FLEXKeychainAccessibilityType;
}
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType {
CFRetain(accessibilityType);
if (FLEXKeychainAccessibilityType) {
CFRelease(FLEXKeychainAccessibilityType);
}
FLEXKeychainAccessibilityType = accessibilityType;
}
#endif
@end
@@ -0,0 +1,112 @@
//
// FLEXKeychainQuery.h
//
// Derived from:
// SSKeychainQuery.h in SSKeychain
// Created by Caleb Davenport on 3/19/13.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#if __IPHONE_7_0 || __MAC_10_9
// Keychain synchronization available at compile time
#define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
#endif
#if __IPHONE_3_0 || __MAC_10_9
// Keychain access group available at compile time
#define FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
typedef NS_ENUM(NSUInteger, FLEXKeychainQuerySynchronizationMode) {
FLEXKeychainQuerySynchronizationModeAny,
FLEXKeychainQuerySynchronizationModeNo,
FLEXKeychainQuerySynchronizationModeYes
};
#endif
/// Simple interface for querying or modifying keychain items.
@interface FLEXKeychainQuery : NSObject
/// kSecAttrAccount
@property (nonatomic, copy) NSString *account;
/// kSecAttrService
@property (nonatomic, copy) NSString *service;
/// kSecAttrLabel
@property (nonatomic, copy) NSString *label;
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
/// kSecAttrAccessGroup (only used on iOS)
@property (nonatomic, copy) NSString *accessGroup;
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/// kSecAttrSynchronizable
@property (nonatomic) FLEXKeychainQuerySynchronizationMode synchronizationMode;
#endif
/// Root storage for password information
@property (nonatomic, copy) NSData *passwordData;
/// This property automatically transitions between an object and the value of
/// `passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
@property (nonatomic, copy) id<NSCoding> passwordObject;
/// Convenience accessor for setting and getting a password string. Passes through
/// to `passwordData` using UTF-8 string encoding.
@property (nonatomic, copy) NSString *password;
#pragma mark Saving & Deleting
/// Save the receiver's attributes as a keychain item. Existing items with the
/// given account, service, and access group will first be deleted.
///
/// @param error Populated should an error occur.
/// @return `YES` if saving was successful, `NO` otherwise.
- (BOOL)save:(NSError **)error;
/// Delete keychain items that match the given account, service, and access group.
///
/// @param error Populated should an error occur.
/// @return `YES` if saving was successful, `NO` otherwise.
- (BOOL)deleteItem:(NSError **)error;
#pragma mark Fetching
/// Fetch all keychain items that match the given account, service, and access
/// group. The values of `password` and `passwordData` are ignored when fetching.
///
/// @param error Populated should an error occur.
/// @return An array of dictionaries that represent all matching keychain items,
/// or `nil` should an error occur. The order of the items is not determined.
- (NSArray<NSDictionary<NSString *, id> *> *)fetchAll:(NSError **)error;
/// Fetch the keychain item that matches the given account, service, and access
/// group. The `password` and `passwordData` properties will be populated unless
/// an error occurs. The values of `password` and `passwordData` are ignored when
/// fetching.
///
/// @param error Populated should an error occur.
/// @return `YES` if fetching was successful, `NO` otherwise.
- (BOOL)fetch:(NSError **)error;
#pragma mark Synchronization Status
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/// Returns a boolean indicating if keychain synchronization is available on the device at runtime.
/// The #define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time.
/// If you are checking for the presence of synchronization, you should use this method.
///
/// @return A value indicating if keychain synchronization is available
+ (BOOL)isSynchronizationAvailable;
#endif
@end
@@ -0,0 +1,304 @@
//
// FLEXKeychainQuery.m
// FLEXKeychain
//
// Created by Caleb Davenport on 3/19/13.
// Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
//
#import "FLEXKeychainQuery.h"
#import "FLEXKeychain.h"
@implementation FLEXKeychainQuery
#pragma mark - Public
- (BOOL)save:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account || !self.passwordData) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = nil;
NSMutableDictionary * searchQuery = [self query];
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
if (status == errSecSuccess) {//item already exists, update it!
query = [[NSMutableDictionary alloc]init];
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
}else if (status == errSecItemNotFound){//item not found, create it!
query = [self query];
if (self.label) {
query[(__bridge id)kSecAttrLabel] = self.label;
}
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
}
return (status == errSecSuccess);
}
- (BOOL)deleteItem:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = [self query];
#if TARGET_OS_IPHONE
status = SecItemDelete((__bridge CFDictionaryRef)query);
#else
// On Mac OS, SecItemDelete will not delete a key created in a different
// app, nor in a different version of the same app.
//
// To replicate the issue, save a password, change to the code and
// rebuild the app, and then attempt to delete that password.
//
// This was true in OS X 10.6 and probably later versions as well.
//
// Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
CFTypeRef result = NULL;
query[(__bridge id)kSecReturnRef] = @YES;
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status == errSecSuccess) {
status = SecKeychainItemDelete((SecKeychainItemRef)result);
CFRelease(result);
}
#endif
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
}
return (status == errSecSuccess);
}
- (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnAttributes] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
return nil;
}
return (__bridge_transfer NSArray *)result;
}
- (BOOL)fetch:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
CFTypeRef result = NULL;
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnData] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
self.passwordData = (__bridge_transfer NSData *)result;
return YES;
}
#pragma mark - Accessors
- (void)setPasswordObject:(id<NSCoding>)object {
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
}
- (id<NSCoding>)passwordObject {
if (self.passwordData.length) {
return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
}
return nil;
}
- (void)setPassword:(NSString *)password {
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
}
- (NSString *)password {
if (self.passwordData.length) {
return [NSString stringWithCString:self.passwordData.bytes encoding:NSUTF8StringEncoding];
}
return nil;
}
#pragma mark - Synchronization Status
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+ (BOOL)isSynchronizationAvailable {
#if TARGET_OS_IPHONE
return YES;
#else
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
#endif
}
#endif
#pragma mark - Private
- (NSMutableDictionary *)query {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
dictionary[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
if (self.service) {
dictionary[(__bridge id)kSecAttrService] = self.service;
}
if (self.account) {
dictionary[(__bridge id)kSecAttrAccount] = self.account;
}
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if !TARGET_IPHONE_SIMULATOR
if (self.accessGroup) {
dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
}
#endif
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([[self class] isSynchronizationAvailable]) {
id value;
switch (self.synchronizationMode) {
case FLEXKeychainQuerySynchronizationModeNo: {
value = @NO;
break;
}
case FLEXKeychainQuerySynchronizationModeYes: {
value = @YES;
break;
}
case FLEXKeychainQuerySynchronizationModeAny: {
value = (__bridge id)(kSecAttrSynchronizableAny);
break;
}
}
dictionary[(__bridge id)(kSecAttrSynchronizable)] = value;
}
#endif
return dictionary;
}
- (NSError *)errorWithCode:(OSStatus)code {
static dispatch_once_t onceToken;
static NSBundle *resourcesBundle = nil;
dispatch_once(&onceToken, ^{
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"FLEXKeychain" withExtension:@"bundle"];
resourcesBundle = [NSBundle bundleWithURL:url];
});
NSString *message = nil;
switch (code) {
case errSecSuccess: return nil;
case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break;
#if TARGET_OS_IPHONE
case errSecUnimplemented: {
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecParam: {
message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecAllocate: {
message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecNotAvailable: {
message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecDuplicateItem: {
message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecItemNotFound: {
message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecInteractionNotAllowed: {
message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecDecode: {
message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecAuthFailed: {
message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"FLEXKeychain", resourcesBundle, nil);
break;
}
default: {
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil);
}
#else
default:
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
#endif
}
NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil;
return [NSError errorWithDomain:kFLEXKeychainErrorDomain code:code userInfo:userInfo];
}
@end
@@ -0,0 +1,14 @@
//
// FLEXKeychainTableViewController.h
// FLEX
//
// Created by ray on 2019/8/17.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
#import "FLEXTableViewController.h"
@interface FLEXKeychainTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -0,0 +1,213 @@
//
// FLEXKeychainTableViewController.m
// FLEX
//
// Created by ray on 2019/8/17.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXUtility.h"
#import "UIPasteboard+FLEX.h"
@interface FLEXKeychainTableViewController ()
@property (nonatomic) NSMutableArray<NSDictionary *> *keychainItems;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXKeychainTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItems = @[
[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed)
],
[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)
],
];
[self refreshkeychainItems];
[self updateHeaderTitle];
}
- (void)refreshkeychainItems
{
self.keychainItems = [FLEXKeychain allAccounts].mutableCopy;
}
- (void)updateHeaderTitle
{
self.headerTitle = [NSString stringWithFormat:@"%@ items", @(self.keychainItems.count)];
}
- (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx
{
NSDictionary *item = self.keychainItems[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = item[kFLEXKeychainWhereKey];
query.account = item[kFLEXKeychainAccountKey];
[query fetch:nil];
return query;
}
- (void)deleteItem:(NSDictionary *)item
{
NSError *error = nil;
BOOL success = [FLEXKeychain
deletePasswordForService:item[kFLEXKeychainWhereKey]
account:item[kFLEXKeychainAccountKey]
error:&error
];
if (!success) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Error Deleting Item");
make.message(error.localizedDescription);
} showFrom:self];
}
}
#pragma mark Buttons
- (void)trashPressed
{
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear Keychain");
make.message(@"This will remove all keychain items for this app.\n");
make.message(@"This action cannot be undone. Are you sure?");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
for (id account in self.keychainItems) {
[self deleteItem:account];
}
[self refreshkeychainItems];
[self.tableView reloadData];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)addPressed
{
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add Keychain Item");
make.textField(@"Service name, i.e. Instagram");
make.textField(@"Account, i.e. username@example.com");
make.textField(@"Password");
make.button(@"Cancel").cancelStyle();
make.button(@"Save").handler(^(NSArray<NSString *> *strings) {
// Display errors
NSError *error = nil;
if (![FLEXKeychain setPassword:strings[2] forService:strings[0] account:strings[1] error:&error]) {
[FLEXAlert showAlert:@"Error" message:error.localizedDescription from:self];
}
[self refreshkeychainItems];
[self.tableView reloadData];
});
} showFrom:self];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row
{
return @"🔑 Keychain";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXKeychainTableViewController *viewController = [self new];
viewController.title = [self globalsEntryTitle:row];
return viewController;
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.keychainItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
}
NSDictionary *item = self.keychainItems[indexPath.row];
id account = item[kFLEXKeychainAccountKey];
if ([account isKindOfClass:[NSString class]]) {
cell.textLabel.text = account;
} else {
cell.textLabel.text = [NSString stringWithFormat:
@"[%@]\n\n%@",
NSStringFromClass([account class]),
[account description]
];
}
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip
{
if (style == UITableViewCellEditingStyleDelete) {
[self deleteItem:self.keychainItems[ip.row]];
[self.keychainItems removeObjectAtIndex:ip.row];
[tv deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(query.service);
make.message(@"Service: ").message(query.service);
make.message(@"\nAccount: ").message(query.account);
make.message(@"\nPassword: ").message(query.password);
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.service];
});
make.button(@"Copy Account").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.account];
});
make.button(@"Copy Password").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.password];
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@end
@@ -0,0 +1,20 @@
Copyright (c) 2010-2012 Sam Soffes.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,207 @@
//
// Taken from https://github.com/llvm-mirror/lldb/blob/master/tools/debugserver/source/MacOSX/DarwinLog/ActivityStreamSPI.h
// by Tanner Bennett on 03/03/2019 with minimal modifications.
//
//===-- ActivityStreamAPI.h -------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef ActivityStreamSPI_h
#define ActivityStreamSPI_h
#include <sys/time.h>
// #include <xpc/xpc.h>
/* By default, XPC objects are declared as Objective-C types when building with
* an Objective-C compiler. This allows them to participate in ARC, in RR
* management by the Blocks runtime and in leaks checking by the static
* analyzer, and enables them to be added to Cocoa collections.
*
* See <os/object.h> for details.
*/
#if !TARGET_OS_MACCATALYST
#if OS_OBJECT_USE_OBJC
OS_OBJECT_DECL(xpc_object);
#else
typedef void * xpc_object_t;
#endif
#endif
#define OS_ACTIVITY_MAX_CALLSTACK 32
// Enums
typedef NS_ENUM(uint32_t, os_activity_stream_flag_t) {
OS_ACTIVITY_STREAM_PROCESS_ONLY = 0x00000001,
OS_ACTIVITY_STREAM_SKIP_DECODE = 0x00000002,
OS_ACTIVITY_STREAM_PAYLOAD = 0x00000004,
OS_ACTIVITY_STREAM_HISTORICAL = 0x00000008,
OS_ACTIVITY_STREAM_CALLSTACK = 0x00000010,
OS_ACTIVITY_STREAM_DEBUG = 0x00000020,
OS_ACTIVITY_STREAM_BUFFERED = 0x00000040,
OS_ACTIVITY_STREAM_NO_SENSITIVE = 0x00000080,
OS_ACTIVITY_STREAM_INFO = 0x00000100,
OS_ACTIVITY_STREAM_PROMISCUOUS = 0x00000200,
OS_ACTIVITY_STREAM_PRECISE_TIMESTAMPS = 0x00000200
};
typedef NS_ENUM(uint32_t, os_activity_stream_type_t) {
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_CREATE = 0x0201,
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_TRANSITION = 0x0202,
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_USERACTION = 0x0203,
OS_ACTIVITY_STREAM_TYPE_TRACE_MESSAGE = 0x0300,
OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE = 0x0400,
OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE = 0x0480,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_BEGIN = 0x0601,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_END = 0x0602,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_EVENT = 0x0603,
OS_ACTIVITY_STREAM_TYPE_STATEDUMP_EVENT = 0x0A00,
};
typedef NS_ENUM(uint32_t, os_activity_stream_event_t) {
OS_ACTIVITY_STREAM_EVENT_STARTED = 1,
OS_ACTIVITY_STREAM_EVENT_STOPPED = 2,
OS_ACTIVITY_STREAM_EVENT_FAILED = 3,
OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED = 4,
OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED = 5,
};
// Types
typedef uint64_t os_activity_id_t;
typedef struct os_activity_stream_s *os_activity_stream_t;
typedef struct os_activity_stream_entry_s *os_activity_stream_entry_t;
#define OS_ACTIVITY_STREAM_COMMON() \
uint64_t trace_id; \
uint64_t timestamp; \
uint64_t thread; \
const uint8_t *image_uuid; \
const char *image_path; \
struct timeval tv_gmt; \
struct timezone tz; \
uint32_t offset
typedef struct os_activity_stream_common_s {
OS_ACTIVITY_STREAM_COMMON();
} * os_activity_stream_common_t;
struct os_activity_create_s {
OS_ACTIVITY_STREAM_COMMON();
const char *name;
os_activity_id_t creator_aid;
uint64_t unique_pid;
};
struct os_activity_transition_s {
OS_ACTIVITY_STREAM_COMMON();
os_activity_id_t transition_id;
};
typedef struct os_log_message_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const uint8_t *buffer;
size_t buffer_sz;
const uint8_t *privdata;
size_t privdata_sz;
const char *subsystem;
const char *category;
uint32_t oversize_id;
uint8_t ttl;
bool persisted;
} * os_log_message_t;
typedef struct os_trace_message_v2_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const void *buffer;
size_t bufferLen;
xpc_object_t __unsafe_unretained payload;
} * os_trace_message_v2_t;
typedef struct os_activity_useraction_s {
OS_ACTIVITY_STREAM_COMMON();
const char *action;
bool persisted;
} * os_activity_useraction_t;
typedef struct os_signpost_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const uint8_t *buffer;
size_t buffer_sz;
const uint8_t *privdata;
size_t privdata_sz;
const char *subsystem;
const char *category;
uint64_t duration_nsec;
uint32_t callstack_depth;
uint64_t callstack[OS_ACTIVITY_MAX_CALLSTACK];
} * os_signpost_t;
typedef struct os_activity_statedump_s {
OS_ACTIVITY_STREAM_COMMON();
char *message;
size_t message_size;
char image_path_buffer[PATH_MAX];
} * os_activity_statedump_t;
struct os_activity_stream_entry_s {
os_activity_stream_type_t type;
// information about the process streaming the data
pid_t pid;
uint64_t proc_id;
const uint8_t *proc_imageuuid;
const char *proc_imagepath;
// the activity associated with this streamed event
os_activity_id_t activity_id;
os_activity_id_t parent_id;
union {
struct os_activity_stream_common_s common;
struct os_activity_create_s activity_create;
struct os_activity_transition_s activity_transition;
struct os_log_message_s log_message;
struct os_trace_message_v2_s trace_message;
struct os_activity_useraction_s useraction;
struct os_signpost_s signpost;
struct os_activity_statedump_s statedump;
};
};
// Blocks
typedef bool (^os_activity_stream_block_t)(os_activity_stream_entry_t entry,
int error);
typedef void (^os_activity_stream_event_block_t)(
os_activity_stream_t stream, os_activity_stream_event_t event);
// SPI entry point prototypes
typedef os_activity_stream_t (*os_activity_stream_for_pid_t)(
pid_t pid, os_activity_stream_flag_t flags,
os_activity_stream_block_t stream_block);
typedef void (*os_activity_stream_resume_t)(os_activity_stream_t stream);
typedef void (*os_activity_stream_cancel_t)(os_activity_stream_t stream);
typedef char *(*os_log_copy_formatted_message_t)(os_log_message_t log_message);
typedef void (*os_activity_stream_set_event_handler_t)(
os_activity_stream_t stream, os_activity_stream_event_block_t block);
#endif /* ActivityStreamSPI_h */
@@ -0,0 +1,18 @@
//
// FLEXASLLogController.h
// FLEX
//
// Created by Tanner on 3/14/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXLogController.h"
@interface FLEXASLLogController : NSObject <FLEXLogController>
/// Guaranteed to call back on the main thread.
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
- (BOOL)startMonitoring;
@end
@@ -0,0 +1,154 @@
//
// FLEXASLLogController.m
// FLEX
//
// Created by Tanner on 3/14/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXASLLogController.h"
#import <asl.h>
// Querying the ASL is much slower in the simulator. We need a longer polling interval to keep things responsive.
#if TARGET_IPHONE_SIMULATOR
#define updateInterval 5.0
#else
#define updateInterval 1.0
#endif
@interface FLEXASLLogController ()
@property (nonatomic, readonly) void (^updateHandler)(NSArray<FLEXSystemLogMessage *> *);
@property (nonatomic) NSTimer *logUpdateTimer;
@property (nonatomic, readonly) NSMutableIndexSet *logMessageIdentifiers;
// ASL stuff
@property (nonatomic) NSUInteger heapSize;
@property (nonatomic) dispatch_queue_t logQueue;
@property (nonatomic) dispatch_io_t io;
@property (nonatomic) NSString *remaining;
@property (nonatomic) int stderror;
@property (nonatomic) NSString *lastTimestamp;
@end
@implementation FLEXASLLogController
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler
{
return [[self alloc] initWithUpdateHandler:newMessagesHandler];
}
- (id)initWithUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler
{
NSParameterAssert(newMessagesHandler);
self = [super init];
if (self) {
_updateHandler = newMessagesHandler;
_logMessageIdentifiers = [NSMutableIndexSet indexSet];
self.logUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval
target:self
selector:@selector(updateLogMessages)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)dealloc
{
[self.logUpdateTimer invalidate];
}
- (BOOL)startMonitoring {
[self.logUpdateTimer fire];
return YES;
}
- (void)updateLogMessages
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<FLEXSystemLogMessage *> *newMessages;
@synchronized (self) {
newMessages = [self newLogMessagesForCurrentProcess];
if (!newMessages.count) {
return;
}
for (FLEXSystemLogMessage *message in newMessages) {
[self.logMessageIdentifiers addIndex:(NSUInteger)message.messageID];
}
self.lastTimestamp = @(asl_get(newMessages.lastObject.aslMessage, ASL_KEY_TIME) ?: "null");
}
dispatch_async(dispatch_get_main_queue(), ^{
self.updateHandler(newMessages);
});
});
}
#pragma mark - Log Message Fetching
- (NSArray<FLEXSystemLogMessage *> *)newLogMessagesForCurrentProcess
{
if (!self.logMessageIdentifiers.count) {
return [self allLogMessagesForCurrentProcess];
}
aslresponse response = [self ASLMessageListForCurrentProcess];
aslmsg aslMessage = NULL;
NSMutableArray<FLEXSystemLogMessage *> *newMessages = [NSMutableArray array];
while ((aslMessage = asl_next(response))) {
NSUInteger messageID = (NSUInteger)atoll(asl_get(aslMessage, ASL_KEY_MSG_ID));
if (![self.logMessageIdentifiers containsIndex:messageID]) {
[newMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
}
asl_release(response);
return newMessages;
}
- (aslresponse)ASLMessageListForCurrentProcess
{
static NSString *pidString = nil;
if (!pidString) {
pidString = @([NSProcessInfo.processInfo processIdentifier]).stringValue;
}
// Create system log query object.
asl_object_t query = asl_new(ASL_TYPE_QUERY);
// Filter for messages from the current process.
// Note that this appears to happen by default on device, but is required in the simulator.
asl_set_query(query, ASL_KEY_PID, pidString.UTF8String, ASL_QUERY_OP_EQUAL);
// Filter for messages after the last retrieved message.
if (self.lastTimestamp) {
asl_set_query(query, ASL_KEY_TIME, self.lastTimestamp.UTF8String, ASL_QUERY_OP_GREATER);
}
return asl_search(NULL, query);
}
- (NSArray<FLEXSystemLogMessage *> *)allLogMessagesForCurrentProcess
{
aslresponse response = [self ASLMessageListForCurrentProcess];
aslmsg aslMessage = NULL;
NSMutableArray<FLEXSystemLogMessage *> *logMessages = [NSMutableArray array];
while ((aslMessage = asl_next(response))) {
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
asl_release(response);
return logMessages;
}
@end
@@ -0,0 +1,19 @@
//
// FLEXLogController.h
// FLEX
//
// Created by Tanner on 3/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FLEXSystemLogMessage.h"
@protocol FLEXLogController <NSObject>
/// Guaranteed to call back on the main thread.
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
- (BOOL)startMonitoring;
@end
@@ -0,0 +1,29 @@
//
// FLEXOSLogController.h
// FLEX
//
// Created by Tanner on 12/19/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXLogController.h"
#define FLEXOSLogAvailable() (NSProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 10)
extern NSString * const kFLEXiOSPersistentOSLogKey;
/// The log controller used for iOS 10 and up.
@interface FLEXOSLogController : NSObject <FLEXLogController>
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
- (BOOL)startMonitoring;
/// Whether log messages are to be recorded and kept in-memory in the background.
/// You do not need to initialize this value, only change it.
@property (nonatomic) BOOL persistent;
/// Used mostly internally, but also used by the log VC to persist messages
/// that were created prior to enabling persistence.
@property (nonatomic) NSMutableArray<FLEXSystemLogMessage *> *messages;
@end
@@ -0,0 +1,220 @@
//
// FLEXOSLogController.m
// FLEX
//
// Created by Tanner on 12/19/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXOSLogController.h"
#include <dlfcn.h>
#include "ActivityStreamAPI.h"
NSString * const kFLEXiOSPersistentOSLogKey = @"com.flex.enablePersistentOSLogLogging";
static os_activity_stream_for_pid_t OSActivityStreamForPID;
static os_activity_stream_resume_t OSActivityStreamResume;
static os_activity_stream_cancel_t OSActivityStreamCancel;
static os_log_copy_formatted_message_t OSLogCopyFormattedMessage;
static os_activity_stream_set_event_handler_t OSActivityStreamSetEventHandler;
static int (*proc_name)(int, char *, unsigned int);
static int (*proc_listpids)(uint32_t, uint32_t, void*, int);
static uint8_t (*OSLogGetType)(void *);
@interface FLEXOSLogController ()
+ (FLEXOSLogController *)sharedLogController;
@property (nonatomic) void (^updateHandler)(NSArray<FLEXSystemLogMessage *> *);
@property (nonatomic) BOOL canPrint;
@property (nonatomic) int filterPid;
@property (nonatomic) BOOL levelInfo;
@property (nonatomic) BOOL subsystemInfo;
@property (nonatomic) os_activity_stream_t stream;
@end
@implementation FLEXOSLogController
+ (void)load
{
// Persist logs when the app launches on iOS 10 if we have persistent logs turned on
if (FLEXOSLogAvailable()) {
BOOL persistent = [[NSUserDefaults standardUserDefaults] boolForKey:kFLEXiOSPersistentOSLogKey];
if (persistent) {
[self sharedLogController].persistent = YES;
[[self sharedLogController] startMonitoring];
}
}
}
+ (instancetype)sharedLogController {
static FLEXOSLogController *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [self new];
});
return shared;
}
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler
{
FLEXOSLogController *shared = [self sharedLogController];
shared.updateHandler = newMessagesHandler;
return shared;
}
- (id)init
{
NSAssert(FLEXOSLogAvailable(), @"os_log is only available on iOS 10 and up");
self = [super init];
if (self) {
_filterPid = NSProcessInfo.processInfo.processIdentifier;
_levelInfo = NO;
_subsystemInfo = NO;
}
return self;
}
- (void)dealloc {
OSActivityStreamCancel(self.stream);
_stream = nil;
}
- (void)setPersistent:(BOOL)persistent {
if (_persistent == persistent) return;
_persistent = persistent;
self.messages = persistent ? [NSMutableArray array] : nil;
}
- (BOOL)startMonitoring {
if (![self lookupSPICalls]) {
// >= iOS 10 is required
return NO;
}
// Are we already monitoring?
if (self.stream) {
// Should we send out the "persisted" messages?
if (self.updateHandler && self.messages.count) {
dispatch_async(dispatch_get_main_queue(), ^{
self.updateHandler(self.messages);
});
}
return YES;
}
// Stream entry handler
os_activity_stream_block_t block = ^bool(os_activity_stream_entry_t entry, int error) {
return [self handleStreamEntry:entry error:error];
};
// Controls which types of messages we see
// 'Historical' appears to just show NSLog stuff
uint32_t activity_stream_flags = OS_ACTIVITY_STREAM_HISTORICAL;
activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
// activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
self.stream = OSActivityStreamForPID(self.filterPid, activity_stream_flags, block);
// Specify the stream-related event handler
OSActivityStreamSetEventHandler(self.stream, [self streamEventHandlerBlock]);
// Start the stream
OSActivityStreamResume(self.stream);
return YES;
}
- (BOOL)lookupSPICalls {
static BOOL hasSPI = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void *handle = dlopen("/System/Library/PrivateFrameworks/LoggingSupport.framework/LoggingSupport", RTLD_NOW);
OSActivityStreamForPID = (os_activity_stream_for_pid_t)dlsym(handle, "os_activity_stream_for_pid");
OSActivityStreamResume = (os_activity_stream_resume_t)dlsym(handle, "os_activity_stream_resume");
OSActivityStreamCancel = (os_activity_stream_cancel_t)dlsym(handle, "os_activity_stream_cancel");
OSLogCopyFormattedMessage = (os_log_copy_formatted_message_t)dlsym(handle, "os_log_copy_formatted_message");
OSActivityStreamSetEventHandler = (os_activity_stream_set_event_handler_t)dlsym(handle, "os_activity_stream_set_event_handler");
proc_name = (int(*)(int, char *, unsigned int))dlsym(handle, "proc_name");
proc_listpids = (int(*)(uint32_t, uint32_t, void*, int))dlsym(handle, "proc_listpids");
OSLogGetType = (uint8_t(*)(void *))dlsym(handle, "os_log_get_type");
hasSPI = (OSActivityStreamForPID != NULL) &&
(OSActivityStreamResume != NULL) &&
(OSActivityStreamCancel != NULL) &&
(OSLogCopyFormattedMessage != NULL) &&
(OSActivityStreamSetEventHandler != NULL) &&
(OSLogGetType != NULL) &&
(proc_name != NULL);
});
return hasSPI;
}
- (BOOL)handleStreamEntry:(os_activity_stream_entry_t)entry error:(int)error {
if (!self.canPrint || (self.filterPid != -1 && entry->pid != self.filterPid)) {
return YES;
}
if (!error && entry) {
if (entry->type == OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE ||
entry->type == OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE) {
os_log_message_t log_message = &entry->log_message;
// Get date
NSDate *date = [NSDate dateWithTimeIntervalSince1970:log_message->tv_gmt.tv_sec];
// Get log message text
const char *messageText = OSLogCopyFormattedMessage(log_message);
// https://github.com/limneos/oslog/issues/1
if (entry->log_message.format && !(strcmp(entry->log_message.format, messageText))) {
messageText = (char *)entry->log_message.format;
}
// move messageText from stack to heap
NSString *msg = [NSString stringWithUTF8String:messageText];
dispatch_async(dispatch_get_main_queue(), ^{
FLEXSystemLogMessage *message = [FLEXSystemLogMessage logMessageFromDate:date text:msg];
if (self.persistent) {
[self.messages addObject:message];
}
if (self.updateHandler) {
self.updateHandler(@[message]);
}
});
}
}
return YES;
}
- (os_activity_stream_event_block_t)streamEventHandlerBlock {
return [^void(os_activity_stream_t stream, os_activity_stream_event_t event) {
switch (event) {
case OS_ACTIVITY_STREAM_EVENT_STARTED:
self.canPrint = YES;
break;
case OS_ACTIVITY_STREAM_EVENT_STOPPED:
break;
case OS_ACTIVITY_STREAM_EVENT_FAILED:
break;
case OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED:
break;
case OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED:
break;
default:
printf("=== Unhandled case ===\n");
break;
}
} copy];
}
@end
@@ -1,6 +1,6 @@
//
// FLEXSystemLogMessage.h
// UICatalog
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
@@ -8,14 +8,23 @@
#import <Foundation/Foundation.h>
#import <asl.h>
#import "ActivityStreamAPI.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXSystemLogMessage : NSObject
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage;
+ (instancetype)logMessageFromDate:(NSDate *)date text:(NSString *)text;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, copy) NSString *sender;
@property (nonatomic, copy) NSString *messageText;
@property (nonatomic, assign) long long messageID;
// ASL specific properties
@property (nonatomic, readonly, nullable) NSString *sender;
@property (nonatomic, readonly, nullable) aslmsg aslMessage;
@property (nonatomic, readonly) NSDate *date;
@property (nonatomic, readonly) NSString *messageText;
@property (nonatomic, readonly) long long messageID;
@end
NS_ASSUME_NONNULL_END
@@ -1,6 +1,6 @@
//
// FLEXSystemLogMessage.m
// UICatalog
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
@@ -12,7 +12,9 @@
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
{
FLEXSystemLogMessage *logMessage = [[FLEXSystemLogMessage alloc] init];
NSDate *date = nil;
NSString *sender = nil, *text = nil;
long long identifier = 0;
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
if (timestamp) {
@@ -21,30 +23,61 @@
if (nanoseconds) {
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
}
logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
}
const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
if (sender) {
logMessage.sender = @(sender);
const char *s = asl_get(aslMessage, ASL_KEY_SENDER);
if (s) {
sender = @(s);
}
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
if (messageText) {
logMessage.messageText = @(messageText);
text = @(messageText);
}
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
if (messageID) {
logMessage.messageID = [@(messageID) longLongValue];
identifier = [@(messageID) longLongValue];
}
return logMessage;
FLEXSystemLogMessage *message = [[self alloc] initWithDate:date sender:sender text:text messageID:identifier];
message->_aslMessage = aslMessage;
return message;
}
+ (instancetype)logMessageFromDate:(NSDate *)date text:(NSString *)text
{
return [[self alloc] initWithDate:date sender:nil text:text messageID:0];
}
- (id)initWithDate:(NSDate *)date sender:(NSString *)sender text:(NSString *)text messageID:(long long)identifier
{
self = [super init];
if (self) {
_date = date;
_sender = sender;
_messageText = text;
_messageID = identifier;
}
return self;
}
- (BOOL)isEqual:(id)object
{
return [object isKindOfClass:[FLEXSystemLogMessage class]] && self.messageID == [object messageID];
if ([object isKindOfClass:[self class]]) {
if (self.messageID) {
// Only ASL uses messageID, otherwise it is 0
return self.messageID == [object messageID];
} else {
// Test message texts and dates for OS Log
return [self.messageText isEqual:[object messageText]] &&
[self.date isEqualToDate:[object date]];
}
}
return NO;
}
- (NSUInteger)hash
@@ -52,4 +85,10 @@
return (NSUInteger)self.messageID;
}
- (NSString *)description
{
NSString *escaped = [self.messageText stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
return [NSString stringWithFormat:@"(%@) %@", @(self.messageText.length), escaped];
}
@end
@@ -1,6 +1,6 @@
//
// FLEXSystemLogTableViewCell.h
// UICatalog
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
@@ -14,7 +14,7 @@ extern NSString *const kFLEXSystemLogTableViewCellIdentifier;
@interface FLEXSystemLogTableViewCell : UITableViewCell
@property (nonatomic, strong) FLEXSystemLogMessage *logMessage;
@property (nonatomic) FLEXSystemLogMessage *logMessage;
@property (nonatomic, copy) NSString *highlightedText;
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage;
@@ -1,6 +1,6 @@
//
// FLEXSystemLogTableViewCell.m
// UICatalog
// FLEX
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
@@ -13,8 +13,8 @@ NSString *const kFLEXSystemLogTableViewCellIdentifier = @"FLEXSystemLogTableView
@interface FLEXSystemLogTableViewCell ()
@property (nonatomic, strong) UILabel *logMessageLabel;
@property (nonatomic, strong) NSAttributedString *logMessageAttributedText;
@property (nonatomic) UILabel *logMessageLabel;
@property (nonatomic) NSAttributedString *logMessageAttributedText;
@end
@@ -24,7 +24,7 @@ NSString *const kFLEXSystemLogTableViewCellIdentifier = @"FLEXSystemLogTableView
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.logMessageLabel = [[UILabel alloc] init];
self.logMessageLabel = [UILabel new];
self.logMessageLabel.numberOfLines = 0;
self.separatorInset = UIEdgeInsetsZero;
self.selectionStyle = UITableViewCellSelectionStyleNone;
@@ -77,9 +77,9 @@ static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
NSDictionary<NSString *, id> *attributes = @{ NSFontAttributeName : [UIFont fontWithName:@"CourierNewPSMT" size:12.0] };
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes];
if ([highlightedText length] > 0) {
if (highlightedText.length > 0) {
NSMutableAttributedString *mutableAttributedText = [attributedText mutableCopy];
NSMutableDictionary<NSString *, id> *highlightAttributes = [@{ NSBackgroundColorAttributeName : [UIColor yellowColor] } mutableCopy];
NSMutableDictionary<NSString *, id> *highlightAttributes = [@{ NSBackgroundColorAttributeName : UIColor.yellowColor } mutableCopy];
[highlightAttributes addEntriesFromDictionary:attributes];
NSRange remainingSearchRange = NSMakeRange(0, text.length);
@@ -118,7 +118,7 @@ static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
static NSDateFormatter *formatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [[NSDateFormatter alloc] init];
formatter = [NSDateFormatter new];
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
});
@@ -1,13 +1,14 @@
//
// FLEXSystemLogTableViewController.h
// UICatalog
// FLEX
//
// Created by Ryan Olson on 1/19/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXSystemLogTableViewController : UITableViewController
@interface FLEXSystemLogTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -1,6 +1,6 @@
//
// FLEXSystemLogTableViewController.m
// UICatalog
// FLEX
//
// Created by Ryan Olson on 1/19/15.
// Copyright (c) 2015 f. All rights reserved.
@@ -8,88 +8,82 @@
#import "FLEXSystemLogTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXSystemLogMessage.h"
#import "FLEXColor.h"
#import "FLEXASLLogController.h"
#import "FLEXOSLogController.h"
#import "FLEXSystemLogTableViewCell.h"
#import <asl.h>
@interface FLEXSystemLogTableViewController () <UISearchResultsUpdating, UISearchControllerDelegate>
@interface FLEXSystemLogTableViewController ()
@property (nonatomic, strong) UISearchController *searchController;
@property (nonatomic, readonly) id<FLEXLogController> logController;
@property (nonatomic, readonly) NSMutableArray<FLEXSystemLogMessage *> *logMessages;
@property (nonatomic, copy) NSArray<FLEXSystemLogMessage *> *filteredLogMessages;
@property (nonatomic, strong) NSTimer *logUpdateTimer;
@property (nonatomic, readonly) NSMutableIndexSet *logMessageIdentifiers;
@end
@implementation FLEXSystemLogTableViewController
- (id)init {
return [super initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
__weak __typeof(self) weakSelf = self;
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
__strong __typeof(weakSelf) self = weakSelf;
[self handleUpdateWithNewMessages:newMessages];
};
_logMessages = [NSMutableArray array];
_logMessageIdentifiers = [NSMutableIndexSet indexSet];
if (FLEXOSLogAvailable()) {
_logController = [FLEXOSLogController withUpdateHandler:logHandler];
} else {
_logController = [FLEXASLLogController withUpdateHandler:logHandler];
}
[self.tableView registerClass:[FLEXSystemLogTableViewCell class] forCellReuseIdentifier:kFLEXSystemLogTableViewCellIdentifier];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.title = @"Loading...";
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@" ⬇︎ " style:UIBarButtonItemStylePlain target:self action:@selector(scrollToLastRow)];
UIBarButtonItem *scrollDown = [[UIBarButtonItem alloc] initWithTitle:@" ⬇︎ "
style:UIBarButtonItemStylePlain
target:self
action:@selector(scrollToLastRow)];
UIBarButtonItem *settings = [[UIBarButtonItem alloc] initWithTitle:@"Settings"
style:UIBarButtonItemStylePlain
target:self
action:@selector(showLogSettings)];
if (FLEXOSLogAvailable()) {
self.navigationItem.rightBarButtonItems = @[scrollDown, settings];
} else {
self.navigationItem.rightBarButtonItem = scrollDown;
}
}
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.delegate = self;
self.searchController.searchResultsUpdater = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.tableView.tableHeaderView = self.searchController.searchBar;
- (void)handleUpdateWithNewMessages:(NSArray<FLEXSystemLogMessage *> *)newMessages
{
self.title = @"System Log";
[self updateLogMessages];
[self.logMessages addObjectsFromArray:newMessages];
// "Follow" the log as new messages stream in if we were previously near the bottom.
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
[self.tableView reloadData];
if (wasNearBottom) {
[self scrollToLastRow];
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSTimeInterval updateInterval = 1.0;
#if TARGET_IPHONE_SIMULATOR
// Querrying the ASL is much slower in the simulator. We need a longer polling interval to keep things repsonsive.
updateInterval = 5.0;
#endif
self.logUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval target:self selector:@selector(updateLogMessages) userInfo:nil repeats:YES];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.logUpdateTimer invalidate];
}
- (void)updateLogMessages
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<FLEXSystemLogMessage *> *newMessages = [self newLogMessagesForCurrentProcess];
if (!newMessages.count) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.title = @"System Log";
[self.logMessages addObjectsFromArray:newMessages];
for (FLEXSystemLogMessage *message in newMessages) {
[self.logMessageIdentifiers addIndex:(NSUInteger)message.messageID];
}
// "Follow" the log as new messages stream in if we were previously near the bottom.
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
[self.tableView reloadData];
if (wasNearBottom) {
[self scrollToLastRow];
}
});
});
[self.logController startMonitoring];
}
- (void)scrollToLastRow
@@ -101,6 +95,36 @@
}
}
- (void)showLogSettings
{
FLEXOSLogController *logController = (FLEXOSLogController *)self.logController;
BOOL persistent = [[NSUserDefaults standardUserDefaults] boolForKey:kFLEXiOSPersistentOSLogKey];
NSString *toggle = persistent ? @"Disable" : @"Enable";
NSString *title = [@"Persistent logging: " stringByAppendingString:persistent ? @"ON" : @"OFF"];
NSString *body = @"In iOS 10 and up, ASL is gone. The OS Log API is much more limited. "
"To get as close to the old behavior as possible, logs must be collected manually at launch and stored.\n\n"
"Turn this feature on only when you need it.";
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(title).message(body).button(toggle).handler(^(NSArray<NSString *> *strings) {
[[NSUserDefaults standardUserDefaults] setBool:!persistent forKey:kFLEXiOSPersistentOSLogKey];
logController.persistent = !persistent;
[logController.messages addObjectsFromArray:self.logMessages];
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"⚠️ System Log";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self new];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
@@ -110,19 +134,19 @@
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.searchController.isActive ? [self.filteredLogMessages count] : [self.logMessages count];
return self.searchController.isActive ? self.filteredLogMessages.count : self.logMessages.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXSystemLogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXSystemLogTableViewCellIdentifier forIndexPath:indexPath];
cell.logMessage = [self logMessageAtIndexPath:indexPath];
cell.highlightedText = self.searchController.searchBar.text;
cell.highlightedText = self.searchText;
if (indexPath.row % 2 == 0) {
cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
cell.backgroundColor = [FLEXColor primaryBackgroundColor];
} else {
cell.backgroundColor = [UIColor whiteColor];
cell.backgroundColor = [FLEXColor secondaryBackgroundColor];
}
return cell;
@@ -149,9 +173,8 @@
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
FLEXSystemLogMessage *logMessage = [self logMessageAtIndexPath:indexPath];
NSString *stringToCopy = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage] ?: @"";
[[UIPasteboard generalPasteboard] setString:stringToCopy];
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = [self logMessageAtIndexPath:indexPath].messageText;
}
}
@@ -160,78 +183,21 @@
return self.searchController.isActive ? self.filteredLogMessages[indexPath.row] : self.logMessages[indexPath.row];
}
#pragma mark - UISearchResultsUpdating
#pragma mark - Search bar
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
- (void)updateSearchResults:(NSString *)searchString
{
NSString *searchString = searchController.searchBar.text;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<FLEXSystemLogMessage *> *filteredLogMessages = [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary<NSString *, id> *bindings) {
[self onBackgroundQueue:^NSArray *{
return [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary<NSString *, id> *bindings) {
NSString *displayedText = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage];
return [displayedText rangeOfString:searchString options:NSCaseInsensitiveSearch].length > 0;
}]];
dispatch_async(dispatch_get_main_queue(), ^{
if ([searchController.searchBar.text isEqual:searchString]) {
self.filteredLogMessages = filteredLogMessages;
[self.tableView reloadData];
}
});
});
}
#pragma mark - Log Message Fetching
- (NSArray<FLEXSystemLogMessage *> *)newLogMessagesForCurrentProcess
{
if (!self.logMessages.count) {
return [[self class] allLogMessagesForCurrentProcess];
}
aslresponse response = [FLEXSystemLogTableViewController ASLMessageListForCurrentProcess];
aslmsg aslMessage = NULL;
NSMutableArray<FLEXSystemLogMessage *> *newMessages = [NSMutableArray array];
while ((aslMessage = asl_next(response))) {
NSUInteger messageID = (NSUInteger)atoll(asl_get(aslMessage, ASL_KEY_MSG_ID));
if (![self.logMessageIdentifiers containsIndex:messageID]) {
[newMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
} thenOnMainQueue:^(NSArray *filteredLogMessages) {
if ([self.searchText isEqual:searchString]) {
self.filteredLogMessages = filteredLogMessages;
[self.tableView reloadData];
}
}
asl_release(response);
return newMessages;
}
+ (aslresponse)ASLMessageListForCurrentProcess
{
static NSString *pidString = nil;
if (!pidString) {
pidString = @([[NSProcessInfo processInfo] processIdentifier]).stringValue;
}
// Create system log query object.
asl_object_t query = asl_new(ASL_TYPE_QUERY);
// Filter for messages from the current process.
// Note that this appears to happen by default on device, but is required in the simulator.
asl_set_query(query, ASL_KEY_PID, pidString.UTF8String, ASL_QUERY_OP_EQUAL);
return asl_search(NULL, query);
}
+ (NSArray<FLEXSystemLogMessage *> *)allLogMessagesForCurrentProcess
{
aslresponse response = [self ASLMessageListForCurrentProcess];
aslmsg aslMessage = NULL;
NSMutableArray<FLEXSystemLogMessage *> *logMessages = [NSMutableArray array];
while ((aslMessage = asl_next(response))) {
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
asl_release(response);
return logMessages;
}];
}
@end
@@ -0,0 +1,276 @@
==============================================================================
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.
==============================================================================
Software from third parties included in the LLVM Project:
==============================================================================
The LLVM Project contains third party software which is under different license
terms. All such code will be identified clearly using at least one of two
mechanisms:
1) It will be in a separate directory tree with its own `LICENSE.txt` or
`LICENSE` file at the top containing the specific license and restrictions
which apply to that software, or
2) It will contain specific license and restriction terms at the top of every
file.
==============================================================================
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2010 Apple Inc.
All rights reserved.
Developed by:
LLDB Team
http://lldb.llvm.org/
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLDB Team, copyright holders, nor the names of
its contributors may be used to endorse or promote products derived from
this Software without specific prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
SOFTWARE.
+4 -4
View File
@@ -8,13 +8,13 @@
#import "FLEXManager.h"
@class FLEXGlobalsTableViewControllerEntry;
@class FLEXGlobalsEntry;
@interface FLEXManager ()
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
@property (nonatomic, readonly, strong) NSArray<FLEXGlobalsTableViewControllerEntry *> *userGlobalEntries;
/// An array of FLEXGlobalsEntry objects that have been registered by the user.
@property (nonatomic, readonly) NSArray<FLEXGlobalsEntry *> *userGlobalEntries;
@property (nonatomic, readonly, strong) NSDictionary<NSString *, FLEXCustomContentViewerFuture> *customContentTypeViewers;
@property (nonatomic, readonly) NSDictionary<NSString *, FLEXCustomContentViewerFuture> *customContentTypeViewers;
@end

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