Compare commits

...

279 Commits

Author SHA1 Message Date
Ryan Olson 8aece0a266 Update README 2015-12-14 08:31:42 -08:00
Ryan Olson 81b27b6918 Modify approach to toggling views and menu modals.
This approach seems to eliminate some of the status bar issues we were seeing.
2015-12-13 23:21:43 -08:00
Ryan Olson 727943c4b3 Merge pull request #92 from WangHengHeng/master
- (void)toggle***Tool - It is not necessary to 'dismiss' when 'present'
2015-12-13 23:12:41 -08:00
Ryan Olson 9f2c032157 List FLEX.h in the public headers in the podspec 2015-12-13 23:06:40 -08:00
Ryan Olson d6a5b1af8d Bump podspec iOS version to 8.0 2015-12-13 23:03:09 -08:00
Ryan Olson dda9dd5beb Move to UISearchController in FLEXSystemLogTableViewController 2015-12-13 22:55:57 -08:00
Ryan Olson 888887f09a Move to UISearchController in FLEXNetworkHistoryTableViewController 2015-12-13 22:46:46 -08:00
Ryan Olson b70a1a2f48 Move to UISearchController in FLEXFileBrowserTableViewController 2015-12-13 22:15:45 -08:00
Ryan Olson 54730c368c Use FLEX.framework in example project
Bump example project deployment target to iOS 8 so we can link to the dynamic framework.
2015-12-13 21:09:20 -08:00
Ryan Olson 21672e6f8d Merge pull request #93 from tttpeng/master
Add a sqlite database browser
2015-12-12 19:11:12 -08:00
Taavo 4ffc992872 Fix some problems about database browser 2015-12-06 04:32:26 +08:00
tttpeng 8eea2ec652 Remove useless methods, About sqlite browser 2015-12-02 12:00:40 +08:00
王 原闯 3df01ee7bb dismiss previous present viewController before present a new one 2015-12-02 10:53:55 +08:00
Ryan Olson d0ad6e4319 Merge pull request #94 from untouchable741/master
Support searching for view pointer address in FLEXHierarchyTableViewC…
2015-12-01 08:26:43 -08:00
Tai Vuong 37aec6dacc Support searching for view pointer address in FLEXHierarchyTableViewController 2015-12-01 22:32:24 +07:00
王 原闯 cdc5aae4b7 - (void)toggle***Tool - It is not necessary to 'dismiss' when 'present' 2015-11-30 18:01:19 +08:00
tttpeng fd2b89fd24 Add sqlite database browser 2015-11-30 17:36:41 +08:00
Ryan Olson f1683e54c3 Add support for force touch in the simulator 2015-11-16 11:37:57 -08:00
Ryan Olson c66dd2e7d3 Merge pull request #85 from dlo/master
Use Objective-C 2.0 subscripting
2015-11-09 06:29:55 -08:00
Ryan Olson 5a5b921bbf Merge pull request #89 from revolter/patch-1
Update FLEXManager.m
2015-11-09 06:27:26 -08:00
Iulian Onofrei 30cc65bd9d Update FLEXManager.m
Fix help screen typo
2015-11-09 11:18:29 +02:00
Dan Loewenherz 29a45aa02d use Objective-C 2.0 subscripting for dictionaries 2015-10-31 17:37:45 -05:00
Dan Loewenherz 08b25ea8d3 use Objective-C 2.0 subscripting for arrays 2015-10-31 17:35:47 -05:00
Ryan Olson 7ffcb83563 Bump version in FLEX.podspec 2015-10-28 20:05:54 -07:00
Ryan Olson b0b64c1ba9 Be robust against nil transactions in FLEXNetworkTransactionTableViewCell 2015-10-28 19:37:41 -07:00
Ryan Olson bc5dfa02ec Merge pull request #84 from robinsonrc/master
Add basic support for viewing shared HTTP cookie storage
2015-10-20 16:31:02 -07:00
Rich Robinson c69f5c220a Update UICatalog project to include FLEXCookiesTableViewController 2015-10-20 07:51:54 +01:00
Rich Robinson 7f28d430d0 Add basic support for viewing shared HTTP cookie storage 2015-10-19 20:05:04 +01:00
Ryan 30dc024903 Add missing runtime import to FLEXUtility 2015-10-10 07:24:54 -07:00
Ryan Olson 6403053989 Merge pull request #81 from orthographic-pedant/spell_check/across
Fixed typographical error, changed accross to across in README.
2015-09-30 14:01:24 -06:00
orthographic-pedant ba352c15e8 Fixed typographical error, changed accross to across in README. 2015-09-30 15:58:40 -04:00
Ryan Olson e9e084e6f1 Use new toggleExplorer convenience method in keyboard shortcut 2015-09-24 12:42:23 -06:00
Ryan Olson 26e92c2bd6 Merge pull request #80 from evliu/master
Convenience method to show/hide explorer depending on current isHidden
2015-09-24 12:37:53 -06:00
Everest Liu 41d761f822 Convenience method to show/hide explorer depending on current isHidden 2015-09-24 10:54:50 -07:00
Ryan Olson 832a03bf27 Bump version in FLEX.podspec 2015-09-21 14:10:26 -06:00
Ryan Olson ebf5254629 Guard usage of class only available in simulator builds 2015-09-21 14:03:16 -06:00
Ryan Olson e199b529c8 Add simulator shortcut info to README 2015-09-21 13:54:08 -06:00
Ryan Olson 06edea64ae Add help menu listing keyboard shortcuts 2015-09-21 13:54:08 -06:00
Ryan Olson 6d3afe36d1 Expand default FLEX keyboard shortcuts 2015-09-21 13:54:08 -06:00
Ryan Olson b22d2b57a2 Add support for keyboard shortcuts in the simulator 2015-09-21 13:54:08 -06:00
Ryan Olson b453086936 Pragma gardening 2015-09-19 21:56:51 -06:00
Ryan Olson 103489c566 Fix header documentatin 2015-09-19 21:56:24 -06:00
Ryan Olson 3dd27557ea Move swizzling helpers into FLEXUtility 2015-09-19 14:10:41 -06:00
Ryan Olson a64188dd5e Simplify network debugging settings
Having two switches to enable network debugging was confusing. One switch now enables network debugging and persists across launches of the app.
2015-09-19 13:56:46 -06:00
Ryan Olson 44e428655a Replace deprecated percent encoding string transformation method 2015-09-19 13:30:30 -06:00
Ryan Olson 96d8b425d5 Bump version in FLEX.podspec 2015-09-10 14:25:42 -06:00
Ryan Olson 7df172afac Make cast explicit 2015-09-10 14:25:08 -06:00
Ryan Olson 9bb44925c8 Bump version in FLEX.podspec 2015-09-09 12:46:11 -06:00
Ryan Olson 320aeb815b Disable app transport security for the example project 2015-09-09 11:45:51 -06:00
Ryan Olson ab4b678498 Fix NSURLSession network observing in iOS 9 2015-09-09 11:45:50 -06:00
Ryan Olson 52bf2071a5 Fix unsafe cast from signed integer to unsigned 2015-09-09 10:38:37 -06:00
Ryan Olson f0bb931a64 Bump version in FLEX.podspec 2015-09-08 10:57:15 -06:00
Ryan Olson 30f1fecc54 Fix crash from trying to read isa fields as Class pointers on arm64
The isa field is not guaranteed to be a Class pointer on arm64, even though the type encoding indicates that it's a Class pointer.
2015-09-08 10:48:58 -06:00
Ryan Olson 85a424a824 Merge pull request #77 from Flipboard/resolve-unused-typedef-warning
Remove unused typedef
2015-09-04 20:01:10 -06:00
Tim Johnsen 6405bf40e3 Remove unused typedef. 2015-09-04 13:55:12 -07:00
Ryan Olson b79fd26ca4 Merge pull request #76 from Flipboard/xcode-7-cleanup
Xcode 7 Cleanup
2015-09-03 15:51:39 -06:00
Ryan Olson caadcce7f1 No longer swizzle dataTaskWithHTTPGetRequest:completionHandler:
This deprecated method has been removed from the NSURLSession header in the iOS 9 SDK, so referencing the selector triggers an undeclared selector warning. We'll follow Apple's lead here and stop supporting the method entirely.
2015-09-03 15:13:14 -06:00
Ryan Olson c250200d03 Use more accurate type for -supportedInterfaceOrientations
The iOS 9 headers fix the return type for -supportedInterfaceOrientations. This mutes an Xcode 7 warning.
2015-09-03 15:10:49 -06:00
Ryan Olson 51c05087e7 Mark -initWithPath: as a designated initializer
Mutes Xcode 7 warning. Marking -initWithPath: as a designated initializer is not allowed in the protocol, so we must redeclare it in the class extension.
2015-09-03 15:08:15 -06:00
Ryan Olson 7a2e65f292 Allow Xcode 7 to make desired changes to the project and scheme files 2015-09-03 15:04:57 -06:00
Ryan Olson 0489c09ba3 Merge pull request #71 from erichoracek/add-framework-support
Add framework support
2015-08-05 11:11:09 -07:00
Eric Horacek 70038d244d Add framework support
- Creats a root-level framework project that builds FLEX.framework
- Adds FLEX shared scheme
- Creates a root .xcworkspace to contain both the framework and example xcproject
- Update .travis.yml to build both framework and example project
- Declares [Carthage](https://github.com/Carthage/Carthage) compatability in README
2015-07-22 07:47:19 -07:00
Ryan Olson a9e0dedd31 Merge pull request #69 from jkyin/master
Update podspec
2015-05-26 07:36:35 -07:00
jkyin f6ad51219d Update podspec 2015-05-26 16:26:07 +08:00
Ryan Olson a42af79040 Protect against a nil NSURLConnection or NSURLSession object being passed to the swizzled delegate methods.
See https://github.com/Flipboard/FLEX/issues/61 for motivation and background.
2015-05-23 14:59:57 -07:00
Ryan Olson 792634527a Merge pull request #68 from modnovolyk/fix-nsurlsessiontask-callback-bug
Fix Network Debugging compatibility with Alamofire
2015-05-19 18:00:02 -07:00
Ryan Olson 5627219c56 Merge pull request #67 from modnovolyk/fix-build-as-framework-issue
Fix build errors while building FLEX as framework for usage in Swift project
2015-05-19 17:53:45 -07:00
Max Odnovolyk 4e81d4b476 Merge branch 'fix-build-as-framework-issue' into fix-nsurlsessiontask-callback-bug
* fix-build-as-framework-issue:
  Make all headers except FLEXManager.h private
  Mute deprecated warning with less pre-processor noise
  Revert changes from fix-build-as-framework-issue branch
2015-05-18 09:17:50 +03:00
Max Odnovolyk 001d58cd89 Make all headers except FLEXManager.h private 2015-05-18 08:37:55 +03:00
Max Odnovolyk 03c96d8fdb Mute deprecated warning with less pre-processor noise 2015-05-18 08:02:39 +03:00
Max Odnovolyk b5423192fb Revert changes from fix-build-as-framework-issue branch 2015-05-18 07:41:31 +03:00
Max Odnovolyk 1efb40a07e Fix NSURLSession task creation with empty completion handler bug 2015-05-17 02:35:17 +03:00
Max Odnovolyk 6c6023dc84 Suppress deprecated-declarations warnings while building as framework via Cocoapods 2015-05-16 03:39:42 +03:00
Max Odnovolyk a6dc4b010c Update FLEX.podspec 2015-05-16 02:53:14 +03:00
Max Odnovolyk dd87da4134 Podspec private_header_files pattern update 2015-05-16 02:08:01 +03:00
Max Odnovolyk 627ff6cbe2 Exclude '*Private*.{h,m}' files from frameworks public headers 2015-05-16 01:58:27 +03:00
Max Odnovolyk 9cc8435cae Hide public asl.h import to prevent 'Include of non-modular header inside framework module' error when building FLEX as framework. 2015-05-16 01:30:43 +03:00
Ryan Olson 3d977450ca Allow the FLEXWindow to become key when it wants to accept input and affect the status bar.
The previous logic was preventing FLEXWindow from ever becoming key.
2015-05-14 10:08:54 -07:00
Ryan Olson f7c482ceed Only allow the FLEXWindow to become key when it has a modal presented.
See https://github.com/Flipboard/FLEX/issues/64 for a more detailed explanation of the motivation for this change.
2015-05-13 21:12:36 -07:00
Ryan Olson c38b90ee60 Change approach to status bar and rotation handling.
Rather than trying to mimic system behavior with status bars and rotation, we can do better by trying to get out of the way entirely. This resolves the UIAlertView/UIAlertController related infinite recursion crashes that started in 8.3. Unfortunately, this approach requires using private API.
2015-04-22 18:38:05 -07:00
Ryan Olson 70491431fa Update CocoaPods example usage 2015-04-16 09:52:32 -07:00
Ryan Olson 6bc055911e Remove redundant contact info from README
Replaced by shield
2015-04-16 09:46:11 -07:00
Ryan Olson 9b1e13b963 Add additional README shields (pod, license, platform, contact) 2015-04-16 09:44:06 -07:00
Ryan Olson 74a73893d4 Add travis shield to README 2015-04-16 09:26:01 -07:00
Ryan Olson 4a3ab17851 Specify simulator in travis.yml 2015-04-16 09:19:12 -07:00
Ryan Olson 5b8efe71a7 Update .travis.yml with Xcode project and scheme 2015-04-15 17:23:15 -07:00
Ryan Olson b7f2d9bcbe Add shared scheme for the example UICatalog xcodeproj
For Travis CI
2015-04-15 17:15:56 -07:00
Ryan Olson f4efc6dbbf Add .travis.yml 2015-04-15 16:04:42 -07:00
Ryan Olson efab760253 Fix setShouldEnableOnLauch: in FLEXNetworkObserver
Doh!
2015-04-01 10:38:28 -07:00
Ryan Olson 48826e2160 Fix readable type encoding for “@?” typically seen with block objects 2015-03-25 23:16:31 -07:00
Fabien Sanglard 0b4e231814 CamelCase directory names that previously had spaces 2015-03-25 09:38:54 -07:00
Ryan Olson 6f2d811338 Improve network history table view performance when lots of network activity is occurring.
This was showing up hot in the profile. The documentation for -[UITableView reloadRowsAtIndexPaths:withRowAnimation:] actually suggests what we’re doing here now for cases where you don’t need to make updates obvious to the user.
2015-03-25 09:22:24 -07:00
Ryan Olson 9c9ce5e2e1 More accurate request timing info if the network recorder queue gets backed up. 2015-03-25 09:22:24 -07:00
Ryan Olson 08b4559b26 Merge pull request #55 from mustafa/fix-imports
fix some imports so we don't depend on pch file
2015-03-24 19:48:41 -07:00
Mustafa Furniturewala 913ad5e2c6 fix some imports so we don't depend on pch file
this is useful if this is being added to a dynamic framework
2015-03-24 11:21:16 -07:00
Ryan Olson 7649dc616c Merge pull request #51 from larrytin/larrytin-patch-1
FLEX requires iOS 7 or higher
2015-03-08 11:53:55 -07:00
田传武 efabb29a52 FLEX requires iOS 7 or higher 2015-03-08 19:40:26 +08:00
Ryan Olson e3612e31d7 Merge pull request #50 from louis-cai/master
update README.md
2015-03-05 22:23:33 -08:00
cailu 2575d2eaee update CocoaPosd version 2015-03-06 11:40:55 +08:00
Ryan Olson f041002e73 Bump version to 2.0.2 2015-03-05 11:07:02 -08:00
Ryan Olson 0da49c1eb6 Avoid trying to thumbnail nil image responses.
These lead to image IO errors in the console log.
2015-03-05 10:59:32 -08:00
Ryan Olson a3a84b0cd7 Merge pull request #48 from DaidoujiChen/feature/json_detect
enhance json detect
2015-03-04 09:35:00 -08:00
DaidoujiChen 33be034e2b enhance json detect
rollback method prettyJSONStringFromData
2015-03-04 15:05:21 +08:00
Ryan Olson f590263d9f Add copy button to request detail view controller.
Copies the text contents of all the rows (i.e. general, request headers, response headers, query parameters, etc.)
2015-03-03 22:42:50 -08:00
Ryan Olson a1c378a9d5 Support copying individual network detail cells via long press 2015-03-03 22:30:50 -08:00
Ryan Olson 617db9c48a Bump version to 2.0.1 2015-02-25 09:47:52 -08:00
Ryan Olson b05f78c388 Merge pull request #47 from judev/patch-1
Fix fileURLOrData passthrough in network observer
2015-02-25 07:55:02 -08:00
Jude Venn d9ecb2359b Fix fileURLOrData passthrough in network observer
Completion handler may be expecting fileURL so should always pass through the response that the session task gave us.
2015-02-25 15:26:59 +00:00
Ryan Olson 5f9a61c755 Show copy menu from long press on network history cells. 2015-02-24 16:38:32 -08:00
Ryan Olson 3dd178c029 Update podspec for v2.0.0 2015-02-24 10:02:26 -08:00
Ryan Olson ed938aa333 Merge pull request #46 from Flipboard/network-debugging
Network Debugging
2015-02-24 09:38:18 -08:00
Ryan Olson 717ed18077 Update README with network history GIF 2015-02-24 09:24:29 -08:00
Ryan Olson 1f43569a4d Protect NSURLSession and NSURLConnection swizzling with dispatch_once 2015-02-24 08:38:35 -08:00
Ryan Olson 991fd6559d Don’t log network events when network debugging is disabled.
Checks already existed for the delegate swizzling path. This change adds checks to the swizzled async convenience methods.
2015-02-24 08:34:04 -08:00
Ryan Olson 9d98993838 Fix NSURLSession and NSURLSessionTask swizzling on iOS 7 2015-02-24 08:03:41 -08:00
Ryan Olson 3eae2ce457 Add confirmation action sheet when clearing requests.
Makes it more clear that the delete has occurred.
2015-02-24 00:20:16 -08:00
Ryan Olson f24edcdba9 Remove NSLogs from FLEXNetworkRecorder
Useful for debugging, but not helpful for users of FLEX.
2015-02-24 00:19:31 -08:00
Ryan Olson a2c7110b07 Modify example network requests to use many different request methods.
Shows that FLEX works no matter what NSURLConnection or NSURLSession API is used for the request.
2015-02-24 00:10:57 -08:00
Ryan Olson 300b68ae83 Swizzle the NSURLConnection and NSURLSessionTask convenience class methods.
Swizzle all the things. This allows us to hear about requests that don’t make the delegate calls.
2015-02-23 23:56:54 -08:00
Ryan Olson ea53ac9bfb Standardize on requestID instead of mixing in requestId
Non-functional change for consistency.
2015-02-23 22:53:17 -08:00
Ryan Olson 6f9c0917eb Refactor FLEXNetworkObserver’s internal state management
Previously, we used PonyDebugger’s approach of keeping a dictionary of request states where the keys were derived from pointers to NSURLConnection objects or NSURLSessionTask objects. This works ok if you’re pretty sure the object will stick around for the whole time you hold the key in the dictionary. However, when we swizzle the async convenience methods on NSURLConnection and NSURLSession, we’ll be less involved in the request lifecycle. Therefore, we need a new approach that guarantees keys will be unique for every NSURLConnection and NSURLSessionTask. To do this, we’re generating UUIDs and attaching them to the connection and task objects via the associated object API. Since we need unique request IDs anyways, this simplifies the request state management nicely.
2015-02-23 22:53:17 -08:00
Ryan Olson b66857f0da Simplify +[FLEXNetworkObserver nextRequestID] 2015-02-23 22:53:17 -08:00
Ryan Olson 52a51b6fd4 Reorder the rows in the request detail view to be more logically grouped by request, then response. 2015-02-23 22:53:17 -08:00
Ryan Olson 06ff0152bb Show an explanatory alert if we can’t display the HTTP body for a request. 2015-02-23 22:53:17 -08:00
Ryan Olson ea1ecbc73a Change wording to refer to “Request Body” rather than “POST Body”
The HTTP method will not necessarily be POST
2015-02-23 22:53:17 -08:00
Ryan Olson 33d2667c99 Don’t clutter logs with NSURLConnection cancelations that occur before willSendRequest:…
These happen quite often and don’t really count as network traffic since they never actually hit the network.
2015-02-23 22:53:17 -08:00
Ryan Olson b333f83470 Actually avoid sending +initialize to all classes when looking for ones we want to swizzle.
class_getInstanceMethod() causes +initialize to classes if they haven’t already received it. By using class_copyMethodList() instead, we avoid +initialize unless we’re actually going to swizzle methods on the class.

This change also takes the running time of the main injection method from ~400 ms to ~50 ms on an iPhone 6. Presumably that’s from +initialize methods that are no longer running here.
2015-02-23 22:53:17 -08:00
Ryan Olson fd54e4cae0 Simplify recorder API by requiring that requestWillBeSent: is called first with the request.
This way, we don’t need to pass the request to every recorder method.
2015-02-23 22:53:11 -08:00
Ryan Olson b1d6ac4d52 Fix response recording for NSURLSessionDownloadTasks
This logic was relying on the request not getting recorded until the first call of URLSession:downloadTask:didWriteData:… Now that we begin recording from -resume, we need a different way to tell if we should record that the response has been received.
2015-02-23 16:23:31 -08:00
Ryan Olson bd9a062329 Swizzle -[NSURLSessionTask resume] as a better requestWillBeSent… hook.
There is not an equivalent to connection:willSendRequest:redirectResponse: in the NSURLSessionDelegate protocol.
2015-02-23 16:23:31 -08:00
Ryan Olson 56fffab825 Fix request duration in error case. 2015-02-23 16:23:31 -08:00
Ryan Olson e73eafccdb Handle NSURLConnection requests that get cancelled. 2015-02-23 16:23:31 -08:00
Ryan Olson f7d23bc8a2 Move setting request mechanism into a separate recorder method.
We need to untangle the mechanism from recordRequestWillBeSent… because we don’t have the full mechanism info (delegate class name) at that time for NSURLSession based requests.
2015-02-23 16:23:31 -08:00
Ryan Olson e531190616 Mute iOS 8 deprecation warnings.
Our current deployment target is iOS 7, so we need to continue using these API (since their replacements are not yet available). However, some people may be using the library with an iOS 8 deployment target, and we don’t want to clutter their builds with warnings.
2015-02-23 16:23:31 -08:00
Ryan Olson a88b5f5019 Better placeholder for requests that don’t have any path components
i.e. http://www.google.com
2015-02-22 11:16:03 -08:00
Ryan Olson 538fe5e17b Allow network history row insertions to finish before starting a new batch of insertions.
This keeps us from stomping on the animation and incorrectly jumping the scroll offset.
2015-02-22 11:02:09 -08:00
Ryan Olson 62a350059b Handle NSURLSessionDataTasks that convert to NSURLSessionDownloadTasks 2015-02-22 10:17:55 -08:00
Ryan Olson 02657d7766 More consistent request mechanism recording 2015-02-22 10:17:55 -08:00
Ryan Olson f13e39450c Properly protect network observer work in the serial queue. 2015-02-22 10:17:06 -08:00
Ryan Olson 1544f2bf4b Update README with network debugging info 2015-02-22 09:32:20 -08:00
Ryan Olson cd41de37e9 Fix recording from URLSession:task:didFinishDownloadingToURL:…
We don’t want to finish the task here because URLSession:task:didCompleteWithError: is still called after this method. We just want to note the data received.
2015-02-22 09:18:35 -08:00
Ryan Olson b4c9cbd6a6 Avoid sending obj-c messages to classes when looking for connection/session delegates.
We don’t want classes to receive +initialize if we can avoid it.
2015-02-22 09:15:13 -08:00
Ryan Olson c5668f42b2 Reload data in network history table after all requests are cleared. 2015-02-21 23:02:14 -08:00
Ryan Olson 99e8f112b2 Remove guards against using API added in iOS 7
Deployment target has been bumped to iOS 7
2015-02-21 23:02:14 -08:00
Ryan Olson 57f4519c3f Bump deployment target to 7.0 2015-02-21 23:02:14 -08:00
Ryan Olson 671f2eeb77 Remove guards around using API added in iOS 5. 2015-02-21 23:02:14 -08:00
Ryan Olson ab31f8202c Add icons for more MIME types 2015-02-21 23:02:14 -08:00
Ryan Olson ffd532b630 Try to parse application/javascript and text/javascript as JSON
Fall back to showing the text without pretty printing.
2015-02-21 23:02:14 -08:00
Ryan Olson f911a91000 Improve messaging when we can’t show a response preview. 2015-02-21 23:02:13 -08:00
Ryan Olson fed8c35f2f Add pragma mark 2015-02-21 23:02:13 -08:00
Ryan Olson aafc23a5ec Add attribution and fix naming in inflate utility method. 2015-02-21 23:02:13 -08:00
Ryan Olson eee45744d0 Better handling when only some of the network recorder methods are called.
We won’t always see the full lifecycle of a request for various reasons. We should still try to log the transaction, even if we only see pieces of it.
2015-02-21 23:02:13 -08:00
Ryan Olson 51493322c3 Add NSULRSessionDataTask to example requests 2015-02-21 23:02:13 -08:00
Ryan Olson baeb0c9a14 Remove unnecessary fake data accumulation for NSURLSessionDownloadTasks 2015-02-21 23:02:13 -08:00
Ryan Olson 141cdf121d Show network debugging settings view controller from a bar button in the history view controller. 2015-02-21 23:02:13 -08:00
Ryan Olson cdb9235a83 Add network debugging settings view controller 2015-02-21 23:02:13 -08:00
Ryan Olson cae0e0ef98 Update the network history view controller when network debugging is enabled/disabled. 2015-02-21 23:02:13 -08:00
Ryan Olson ec98b0cba6 Update network history UI when the recorder is cleared. 2015-02-21 23:02:12 -08:00
Ryan Olson d468254b21 Interface naming consistency 2015-02-21 23:02:12 -08:00
Ryan Olson 462e0ed548 Update filtered bytes received when the network transactions change.
Previously we only updated the main bytes received state, but both can change when the network transactions change.
2015-02-21 23:02:12 -08:00
Ryan Olson cbfc49bd0c Add API to the network recorder to clear recorder activity
Post a notification when finished so UI that shows the network activity can update.
2015-02-21 23:02:12 -08:00
Ryan Olson 176652010c Add API to the network observer to enable on launch.
The setting is persisted in NSUserDefaults, so it allows user to choose to leave FLEX network debugging on for longer than the current session.
2015-02-21 23:02:12 -08:00
Ryan Olson 2e516615bd Remove unused @class delcaration 2015-02-21 23:02:12 -08:00
Ryan Olson 6d1db9535f Add setting to network recorder to disable caching media response bodies.
i.e. image, video, audio. Most of the time, we’re not interested in looking at these response bodies, and they occupy lots space in the cache. Data responses (json, xml, etc.) tend to be more valuable to keep around.
2015-02-21 23:02:12 -08:00
Ryan Olson 09bc0377d8 Change default recorder cache maximum to 25 MB
Reduce the memory impact of the tool
2015-02-21 23:02:12 -08:00
Ryan Olson 197dcccb4e Use decompression utility for Content-Encoding: gzip 2015-02-21 23:02:12 -08:00
Ryan Olson e082fdeddd Fix method naming on decompression utility. 2015-02-21 23:02:12 -08:00
Ryan Olson e4e22b8cd8 Bump the font size on the main menu.
We’re intentionally tiny elsewhere to get more information on screen, but here we can go a little bigger.
2015-02-21 23:02:11 -08:00
Ryan Olson 11bb3574a8 Show network debugging status in history header when disabled 2015-02-21 23:02:11 -08:00
Ryan Olson 9ed5e08089 Show header in network history with number of requests and total bytes received. 2015-02-21 23:02:11 -08:00
Ryan Olson 01abdf05ed Add network transaction detail section for POST body data 2015-02-21 23:02:11 -08:00
Ryan Olson 65e6e86378 Support repeated parameters in query string parsing (arrays) 2015-02-21 23:02:11 -08:00
Ryan Olson 2b3677e503 Add method to get deflated data from compressed data.
Requires linking to libz
2015-02-21 23:02:11 -08:00
Ryan Olson 84dcbb4847 Improve response body detail view controller logic 2015-02-21 23:02:11 -08:00
Ryan Olson cca6415c2a Make FLEXUtility JSON prettifying more robust.
NSJSONSerialization loves to throw exceptions.
2015-02-21 23:02:11 -08:00
Ryan Olson 73f3d52bb3 Generalize utility method to generate dictionary from query data
We’ll use it for post bodies with application/x-www-form-urlencoded content types.
2015-02-21 23:02:11 -08:00
Ryan Olson 4e167f02fb Properly guard use of FLEX in the example project with #if DEBUG 2015-02-21 23:02:11 -08:00
Ryan Olson ec76385bcf Add NSURLSession request to the example project 2015-02-21 23:02:10 -08:00
Ryan Olson 387a72d93e Appease Xcode by adding more launch images 2015-02-21 23:02:10 -08:00
Ryan Olson b8d8f29c97 Record/show “request mechanism” for requests.
i.e. NSURLConnection (delegate: AFURLConnectionOperation)
2015-02-21 23:02:10 -08:00
Ryan Olson 8e7dfe9adb Properly guard swizzled methods to avoid duplicate sniffing 2015-02-21 23:02:10 -08:00
Ryan Olson 6471a96b19 Fix incorrect argument type in swizzled NSURLSession delegate method. 2015-02-21 23:02:10 -08:00
Ryan Olson 3b66d9b53e Pass delegate along to all swizzled connection and session methods. 2015-02-21 23:02:10 -08:00
Ryan Olson 3385493b6a Remove ineffective swizzle guard 2015-02-21 23:02:10 -08:00
Ryan Olson 993b61ecb6 Bail in the network recorder if we get messages for a request we don’t know about.
At the moment, we’re requiring that recordRequstWillBeSentWithRequestId:… is called for every request. We could consider relaxing that requirement and doing more inference, but we will lose some information (e.g. timing) if we allow that.
2015-02-21 23:02:10 -08:00
Ryan Olson ea5cea6d77 Support showing responses for application/x-plist MIME types. 2015-02-21 23:02:10 -08:00
Ryan Olson 47db763ea6 Add error row to the request detail view controller. 2015-02-21 23:02:10 -08:00
Ryan Olson 054cfd196a Ignore CLTilesManagerClient when swizzling to eliminate sandbox violation warnings. 2015-02-21 23:02:09 -08:00
Ryan Olson 4d9271c7ed Add pass-through API to FLEXManager for setting the response cache limit
Clients should only need to know about FLEXManager.
2015-02-21 23:02:09 -08:00
Ryan Olson 719c641692 Support a customizable limit on the network response cache. 2015-02-21 23:02:09 -08:00
Ryan Olson 95eaab3486 Set titles for web views pushed onto the navigation stack. 2015-02-21 23:02:09 -08:00
Ryan Olson b4d03fc9e9 Unescape forward slashes when making pretty JSON.
NSJSONSerialization escapes these but forward slashes are valid in JSON. When we display the JSON in a web view, it is helpful to have URLs without the escaping to make the web view’s URL data detector work.
2015-02-21 23:02:09 -08:00
Ryan Olson 3b5ed964b2 Set a better initial zoom scale when using a web view to display text. 2015-02-21 23:02:09 -08:00
Ryan Olson 67a91b5283 Update the transaction detail table view controller when properties on its transaction are updated. 2015-02-21 23:02:09 -08:00
Ryan Olson 580a4d8a1c Add request start time to the network transaction detail interface 2015-02-21 23:02:09 -08:00
Ryan Olson ad0b130dc4 Improve network history interface when new requests are fired.
If the user is at the top of the table, we animate the insertion of the new row. If they have scrolled down the list, we maintain their relative position while adding the new row at the top without animation.
2015-02-21 23:02:09 -08:00
Ryan Olson 5ecdb953c7 Handle redirect responses in the network recorder. 2015-02-21 23:02:09 -08:00
Ryan Olson c7132f5890 Be robust to nil body data in recordLoadingFinishedWithRequestId:responseBody: 2015-02-21 23:02:08 -08:00
Ryan Olson 6d680e4df7 Order network transactions from newest first.
We’ll generally be more interested in the most recent requests.
2015-02-21 23:02:08 -08:00
Ryan Olson 18e2edb15f Reload the relevant network transaction cell in the history view controller when the transaction updates. 2015-02-21 23:02:08 -08:00
Ryan Olson f4106e4be5 Update search results when a new network request matching the search term comes in. 2015-02-21 23:02:08 -08:00
Ryan Olson a53f744035 Improve thread safety in the network recorder.
The recorder methods can be called from any thread, so we need to be careful around the internal mutable structures that are not thread safe. The bulk of this diff is from indentation changes.
2015-02-21 23:02:08 -08:00
Ryan Olson 61bcf5354a Post recorder notifications on the main thread so observers don’t need to worry about which thread the notification is posted on. 2015-02-21 23:02:08 -08:00
Ryan Olson dd686e80da Add example network requests to the sample project. 2015-02-21 23:02:08 -08:00
Ryan Olson 794d00d82f Add network debugging files to example project 2015-02-21 23:02:00 -08:00
Ryan Olson e9801242f5 Push the transaction detail view controller for taps on transaction cells in the history view controller. 2015-02-21 22:45:56 -08:00
Ryan Olson 11411a20b7 Add convenience method for getting the network transaction for an index path in the history view controller. 2015-02-21 22:45:56 -08:00
Ryan Olson db089f4fb9 Add detial view controller for network transactions 2015-02-21 22:45:56 -08:00
Ryan Olson 34721971c5 Make use of new request related FLEXUtility methods 2015-02-21 22:45:56 -08:00
Ryan Olson 26f80349f6 Add network request/response helpers to FLEXUtility 2015-02-21 22:45:56 -08:00
Ryan Olson 190006dcc0 Replace FLEXDescriptionTableViewCell with a more generic multiline cell class. 2015-02-21 22:45:56 -08:00
Ryan Olson 1d7d41a350 Add API to enable network debugging from FLEXManager.
This way clients don’t need to know anything about FLEXNetworkObserver or FLEXNetworkRecorder
2015-02-21 22:45:55 -08:00
Ryan Olson d74bc56487 Add public API to determine if FLEXNetworkObserver is enabled.
Also post a notification when the enabled state changes.
2015-02-21 22:45:55 -08:00
Ryan Olson fa4eeea424 Add search bar filtering to the network history view controller. 2015-02-21 22:45:55 -08:00
Ryan Olson 3c8ac29ebe Add table view controller and cell subclasses to show network history 2015-02-21 22:45:55 -08:00
Ryan Olson 18487eeac5 Add utility method for generating request duration strings. 2015-02-21 22:45:55 -08:00
Ryan Olson b040530219 Use icons based on MIME type as thumbnails for non-image responses. 2015-02-21 22:45:55 -08:00
Ryan Olson 7fb22825ae Add icons for network responses that are json, html, or text/plain
Skipping the 1x versions for now. We’ll just scale down the 2x assets on non-retina screens.
2015-02-21 22:45:55 -08:00
Ryan Olson 61fe88fd7f For image downloads, stash a thumbnail preview of the response when it finishes. 2015-02-21 22:45:55 -08:00
Ryan Olson 60226cc62b Add utility method for generating thumbnails from image data. 2015-02-21 22:45:55 -08:00
Ryan Olson fad33ddf7a Post notifications when new network requests are recorded and when requests are updated.
This will allow any UI which is displaying the requests to update appropriately.
2015-02-21 22:45:54 -08:00
Ryan Olson 630eec59ee Add classes for observing and recording network events.
Big thanks to PonyDebugger for doing the heavy lifting here. FLEXNetworkObserver is adapted from PDNetworkDomainController.
2015-02-21 22:45:54 -08:00
Ryan Olson 4e096a2cd7 Title case capitalization OCD 2015-02-18 09:22:07 -08:00
Ryan Olson 28241518bf Remove unnecessary method declaration 2015-02-18 09:19:39 -08:00
Ryan Olson e1c2c9c6fc Add emoji for menu consistency 2015-02-18 09:17:58 -08:00
Ryan Olson ef96471937 Only call view controller future block once per push.
Avoids creating two view controllers when we only need one.
2015-02-18 09:17:39 -08:00
Ryan Olson 4b9385bee0 Merge pull request #42 from maniak-dobrii/master
Added ability to add UIViewController based user defined global entries.
2015-02-18 09:12:28 -08:00
Mikhail Solodovnichenko 9d68ff5d15 Removed redundant import 2015-02-18 15:56:15 +03:00
Mikhail Solodovnichenko a644f89bbd Separated register global entry API to object and viewcontroller parts. 2015-02-18 15:48:53 +03:00
Ryan Olson ed9b721cd5 Add .clang-format file to the project 2015-02-17 19:32:37 -08:00
Ryan Olson 2b1f28c2fd Clang format pass on FLEXFileBrowserTableViewController 2015-02-17 19:31:57 -08:00
Ryan Olson 6d563856a6 Add comment explaining why the file browser menu actions aren’t implemented in the table view delegate method. 2015-02-17 19:23:48 -08:00
Ryan Olson 491ba78729 Merge branch 'filesystem-actions' 2015-02-17 19:17:52 -08:00
Daniel Rodríguez Troitiño d978d574ad Add rename and delete operations to file browser. 2015-02-17 19:17:02 -08:00
Ryan Olson a2cd7d81ed Merge pull request #45 from drodriguez/date-editor
Add editor for NSDate values.
2015-02-16 21:38:03 -08:00
Daniel Rodríguez Troitiño e0366afcc2 Add editor for NSDate values.
The argument input for dates is an UIDatePicker, set to use gregorian
calendar and UTC time zone (the locale is still the current one).

Unfortunately UIDatePicker don’t give the option for showing seconds.

Works in the object explorer and the defaults explorer. The changes
around ArgumentInputViewFactory and DefaultEditorVC allows to introspect
the value for its class and show the right editor (otherwise the JSON
editor is used by default).
2015-02-14 15:49:16 -08:00
Mikhail Solodovnichenko 1653d86552 Added ability to add UIViewController based user defined global enties. 2015-02-10 12:34:49 +03:00
Ryan Olson 05399839a3 Show all system windows in the view hierarchy list.
Previously we showed the application’s windows plus the status bar window, but this leaves out things like the keyboard, alert windows, etc.
2015-02-08 18:35:57 -08:00
Ryan Olson 49afafc50a Mute warning 2015-02-05 17:17:05 -08:00
Ryan Olson 063ffe97c1 Fix system log viewer on iOS 7
The availability/deprecation annotations in asl.h (8.1 SDK) are incorrect. asl_next() and asl_release() were added in iOS 8, not iOS 7.
2015-01-26 13:28:28 -08:00
Ryan Olson 35938ecbee Update README with log viewing feature 2015-01-26 10:48:02 -08:00
Ryan Olson 9999f6b1d3 Merge branch 'asl-viewer' 2015-01-26 10:43:42 -08:00
Ryan Olson 93d759e743 Support native iPhone 6 and 6+ resolutions in the example project.
Thanks to @_DavidSmith for the placeholder images: https://t.co/Cyh6Ve32Ab
2015-01-26 10:40:26 -08:00
Ryan Olson d989001073 Support copying log messages on cell long-presses. 2015-01-26 10:32:32 -08:00
Ryan Olson ad231a5bb7 Add dummy log messages to the example project to demonstrate the system log interface. 2015-01-26 10:18:13 -08:00
Ryan Olson b67d18f569 Add interface for viewing system logs from FLEX 2015-01-26 10:03:24 -08:00
Ryan Olson f65263d027 Change “globals” toolbar item to “menu”
There’s a lot in this list now. It’s no longer just singletons and global state.
2015-01-21 12:24:52 -08:00
Ryan Olson 3870f7c7cf Merge pull request #39 from tijoinc/copy-object-description
Copy object description
2015-01-21 12:21:08 -08:00
Tim Johnsen 88c893cc44 Add the ability to long press on object descriptions to copy their contents. 2015-01-21 10:09:41 -08:00
Ryan Olson 53a95845f4 Improve description for ivars and properties of type SEL
Show the selector name rather then the pointer address.
2015-01-19 14:56:41 -08:00
Ryan Olson 8f6b8b2dae Add preview image option for CALayer objects 2014-12-14 20:14:57 -08:00
Ryan Olson fefcd125b5 Merge pull request #33 from studentdeng/master
[BUG FIX] fix iOS8 load all classes failed
2014-11-18 19:11:42 -08:00
studentdeng 5fb6684b31 [BUG FIX] fix iOS8 load all classes failed 2014-11-18 20:41:12 +08:00
Ryan Olson 7a93e9b4dd Fix view selection when not in portrait.
Thanks to @lascorbe for discovering this and suggesting a fix (#31).
2014-10-02 10:10:41 -07:00
Ryan Olson a04cdf789a Safely clear UIWebView’s delegate in FLEXWebViewController. 2014-09-26 12:02:20 -07:00
Ryan Olson 9d1489adbe Remove unnecessary call to add FLEX’s root view controller’s view to the window.
This is done automatically by setting the rootViewController property on the window.
2014-09-26 11:54:36 -07:00
Ryan Olson 10c7b7b420 Support multiple levels of child view controller forwarding for status bar properties. 2014-09-26 11:54:36 -07:00
Ryan Olson 8bf66eb664 Merge pull request #29 from Flipboard/raphaelschaad-whitespace-fix
Whitespace fix
2014-09-25 15:17:52 -07:00
Raphael Schaad 43eedfafc5 Fix whitespace to make it consistent with rest of code base. 2014-09-25 15:14:58 -07:00
Ryan Olson c6b2e97835 Remove "Changes" section from README
Replaced by Github's releases feature.
2014-08-24 23:36:15 -07:00
Ryan Olson c6b0a74873 Bump version in podspec 2014-08-24 23:35:19 -07:00
Ryan Olson 7054919e3e Respect child view controller status bar forwarding 2014-08-24 22:40:07 -07:00
Ryan Olson fe2ee7670f Add installation section to README 2014-08-24 22:11:44 -07:00
Ryan Olson 27377415a8 Merge pull request #23 from studentdeng/develop
add a default File Browser
2014-08-20 00:04:48 -07:00
studentdeng 6f26164ea3 add a default File Browser 2014-08-14 13:46:32 +08:00
Ryan Olson 4a243adef8 Merge pull request #22 from DaidoujiChen/develop
Hide keyboard when search button is pressed
2014-08-13 20:20:16 -07:00
DaidoujiChen d226b8d67e when press "Search" button, searchBar need to resignFirstResponder 2014-08-11 16:31:54 +08:00
Ryan Olson e968c3bd1a Use explicit nil initialization for consistency within the project. 2014-08-05 22:57:00 -07:00
Ryan Olson 36c88e341a Fix “dead store” warning from the clang static analyzer.
We only use fullPath later. Subpath is only used temporarily to build the fullPath in one of the cases.
2014-08-05 22:48:01 -07:00
Ryan Olson 5dc35442a8 Update changes and todo sections in the README 2014-08-05 22:40:40 -07:00
Ryan Olson af8ffc8c76 Merge pull request #15 from DaidoujiChen/develop
Search bar filtering and sorting by file size in the file browser
2014-08-05 22:37:27 -07:00
DaidoujiChen 77fcccc546 Search bar filtering and sorting by file size in the file browser 2014-08-06 09:59:26 +08:00
Ryan Olson 86516a9c37 Rename variables in inner scope to avoid shadowing outer variables of the same name.
Discovered with -Wshadow
2014-08-05 16:11:05 -07:00
Ryan Olson 59a6080c73 Don't rely on UIKit.h being imported from a PCH 2014-08-03 13:26:08 -07:00
Ryan Olson 27abb6fa10 Merge pull request #14 from JaviSoto/feature/lazy-window-creation
Lazily creating the FLEXWindow when it's actually needed.
2014-08-02 14:29:37 -07:00
Ryan Olson f956f85190 Eliminate ambiguity when using the property, ivar, and method boxes.
Some users have reported build errors in these areas. They likely have conflicting methods that get imported through a PCH. For example, if the compiler sees a definition of another method called “property” that returns an NSString, it doesn’t know which one to pick.
2014-08-02 14:18:03 -07:00
Javier Soto 9ae13361e9 Also create the explorer view controller lazily. 2014-08-01 16:17:26 -07:00
Ryan Olson 5a312d12f8 Add FLEXFloor macro for retina aware rounding.
This allows us to move views to 0.5 point origin values on retina devices.
2014-07-31 20:12:13 -07:00
Javier Soto f3c53dde2c Lazily creating the FLEXWindow when it's actually needed.
This allows users of FLEXManager to use the -registerGlobalEntryWithName:objectFutureBlock: API in a +load method of their app. Right now that creates the UIWindow object when UIKit is not ready. With this change, the window will be created the first time you call -showExplorer, which would also help a bit with launch times in apps that have FLEX because creating that object would be delayed from app launch to the first time it's used.
2014-07-31 12:11:15 -07:00
167 changed files with 9097 additions and 1089 deletions
+47
View File
@@ -0,0 +1,47 @@
---
BasedOnStyle: WebKit
AccessModifierOffset: -2
AlignEscapedNewlinesLeft: false
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackParameters: true
BreakBeforeBinaryOperators: false
BreakBeforeBraces: Linux
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 0
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerBinding: false
ExperimentalAutoDetectBinPacking: false
IndentCaseLabels: true
IndentFunctionDeclarationAfterType: false
IndentWidth: 4
MaxEmptyLinesToKeep: 3
NamespaceIndentation: None
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 60
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerBindsToType: false
SpaceAfterControlStatementKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
Standard: Auto
TabWidth: 4
UseTab: Never
...
+8
View File
@@ -0,0 +1,8 @@
language: objective-c
xcode_workspace: FLEX.xcworkspace
matrix:
include:
- xcode_scheme: UICatalog
xcode_sdk: iphonesimulator
- xcode_scheme: FLEX
xcode_sdk: iphonesimulator
@@ -69,7 +69,7 @@
[self.valueLabel sizeToFit];
CGFloat valueLabelOriginX = CGRectGetMaxX(self.slider.frame);
CGFloat valueLabelOriginY = floor((self.slider.frame.size.height - self.valueLabel.frame.size.height) / 2.0);
CGFloat valueLabelOriginY = FLEXFloor((self.slider.frame.size.height - self.valueLabel.frame.size.height) / 2.0);
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
}
@@ -0,0 +1,13 @@
//
// FLEXArgumentInputDataView.h
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/14/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputView.h"
@interface FLEXArgumentInputDateView : FLEXArgumentInputView
@end
@@ -0,0 +1,63 @@
//
// FLEXArgumentInputDataView.m
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/14/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputDateView.h"
#import "FLEXRuntimeUtility.h"
@interface FLEXArgumentInputDateView ()
@property (nonatomic, strong) UIDatePicker *datePicker;
@end
@implementation FLEXArgumentInputDateView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.datePicker = [[UIDatePicker alloc] init];
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
// Using UTC, because that's what the NSDate description prints
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
self.datePicker.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
[self addSubview:self.datePicker];
}
return self;
}
- (void)setInputValue:(id)inputValue
{
if ([inputValue isKindOfClass:[NSDate class]]) {
self.datePicker.date = inputValue;
}
}
- (id)inputValue
{
return self.datePicker.date;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.datePicker.frame = self.bounds;
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat height = [self.datePicker sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, FLEXEncodeClass(NSDate)) == 0)) || [value isKindOfClass:[NSDate class]];
}
@end
@@ -31,7 +31,7 @@
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
if (fieldIndex < [customTitles count]) {
inputView.title = [customTitles objectAtIndex:fieldIndex];
inputView.title = customTitles[fieldIndex];
} else {
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
}
@@ -72,7 +72,7 @@
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
void *fieldPointer = unboxedValue + fieldOffset;
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
inputView.inputValue = (__bridge id)fieldPointer;
@@ -102,7 +102,7 @@
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
void *fieldPointer = unboxedStruct + fieldOffset;
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
// Object fields
@@ -12,9 +12,12 @@
@interface FLEXArgumentInputViewFactory : NSObject
/// The main factory method for making argument input view subclasses that are the best fit for the type.
/// Forwards to argumentInputViewForTypeEncoding:currentValue: with a nil currentValue.
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding;
/// The main factory method for making argument input view subclasses that are the best fit for the type.
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
/// A way to check if we should try editing a filed given its type encoding and value.
/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
@@ -16,12 +16,18 @@
#import "FLEXArgumentInputStringView.h"
#import "FLEXArgumentInputFontView.h"
#import "FLEXArgumentInputColorView.h"
#import "FLEXArgumentInputDateView.h"
@implementation FLEXArgumentInputViewFactory
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
{
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:nil];
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
}
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
if (!subclass) {
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
// The unsupported view shows "nil" and does not allow user input.
@@ -47,6 +53,8 @@
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]) {
@@ -41,10 +41,11 @@
[super viewDidLoad];
self.fieldEditorView.fieldDescription = self.key;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id)];
id currentValue = [self.defaults objectForKey:self.key];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id) currentValue:currentValue];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [self.defaults objectForKey:self.key];
inputView.inputValue = currentValue;
self.fieldEditorView.argumentInputViews = @[inputView];
}
-31
View File
@@ -1,31 +0,0 @@
//
// FLEXManager.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, readonly) BOOL isHidden;
- (void)showExplorer;
- (void)hideExplorer;
#pragma mark - Extensions
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
/// @note This method must be called from the main thread.
/// The objectFutureBlock will be invoked from the main thread and may return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id(^)(void))objectFutureBlock;
@end
-105
View File
@@ -1,105 +0,0 @@
//
// FLEXManager.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXManager.h"
#import "FLEXExplorerViewController.h"
#import "FLEXWindow.h"
#import "FLEXGlobalsTableViewControllerEntry.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
@property (nonatomic, strong) FLEXWindow *explorerWindow;
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
@end
@implementation FLEXManager
+ (instancetype)sharedManager
{
static FLEXManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[[self class] alloc] init];
});
return sharedManager;
}
- (instancetype)init
{
self = [super init];
if (self) {
self.explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.explorerWindow.eventDelegate = self;
_userGlobalEntries = [[NSMutableArray alloc] init];
self.explorerViewController = [[FLEXExplorerViewController alloc] init];
self.explorerViewController.delegate = self;
self.explorerWindow.rootViewController = self.explorerViewController;
[self.explorerWindow addSubview:self.explorerViewController.view];
}
return self;
}
- (void)showExplorer
{
self.explorerWindow.hidden = NO;
}
- (void)hideExplorer
{
self.explorerWindow.hidden = YES;
}
- (BOOL)isHidden
{
return self.explorerWindow.isHidden;
}
#pragma mark - FLEXWindowEventDelegate
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
{
// Ask the explorer view controller
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
}
#pragma mark - FLEXExplorerViewControllerDelegate
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
{
[self hideExplorer];
}
#pragma mark - Extensions
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
{
NSParameterAssert(entryName);
NSParameterAssert(objectFutureBlock);
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
entryName = entryName.copy;
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
return entryName;
} viewControllerFuture:^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
}];
[self.userGlobalEntries addObject:entry];
}
@end
-36
View File
@@ -1,36 +0,0 @@
//
// FLEXWindow.m
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXWindow.h"
@implementation FLEXWindow
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
// UIWindowLevelStatusBar + 100 seems to hit that balance.
self.windowLevel = UIWindowLevelStatusBar + 100.0;
}
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
}
return pointInside;
}
@end
@@ -9,6 +9,7 @@
#import "FLEXExplorerToolbar.h"
#import "FLEXToolbarItem.h"
#import "FLEXResources.h"
#import "FLEXUtility.h"
@interface FLEXExplorerToolbar ()
@@ -46,7 +47,7 @@
[self.dragHandle addSubview:self.dragHandleImageView];
UIImage *globalsIcon = [FLEXResources globeIcon];
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"globals" image:globalsIcon];
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"menu" image:globalsIcon];
[self addSubview:self.globalsItem];
[toolbarItems addObject:self.globalsItem];
@@ -98,8 +99,8 @@
const CGFloat kToolbarItemHeight = [[self class] toolbarItemHeight];
self.dragHandle.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, [[self class] dragHandleWidth], kToolbarItemHeight);
CGRect dragHandleImageFrame = self.dragHandleImageView.frame;
dragHandleImageFrame.origin.x = floor((self.dragHandle.frame.size.width - dragHandleImageFrame.size.width) / 2.0);
dragHandleImageFrame.origin.y = floor((self.dragHandle.frame.size.height - dragHandleImageFrame.size.height) / 2.0);
dragHandleImageFrame.origin.x = FLEXFloor((self.dragHandle.frame.size.width - dragHandleImageFrame.size.width) / 2.0);
dragHandleImageFrame.origin.y = FLEXFloor((self.dragHandle.frame.size.height - dragHandleImageFrame.size.height) / 2.0);
self.dragHandleImageView.frame = dragHandleImageFrame;
@@ -107,7 +108,7 @@
CGFloat originX = CGRectGetMaxX(self.dragHandle.frame);
CGFloat originY = self.bounds.origin.y;
CGFloat height = kToolbarItemHeight;
CGFloat width = floor((CGRectGetMaxX(self.bounds) - originX) / [self.toolbarItems count]);
CGFloat width = FLEXFloor((CGRectGetMaxX(self.bounds) - originX) / [self.toolbarItems count]);
for (UIView *toolbarItem in self.toolbarItems) {
toolbarItem.frame = CGRectMake(originX, originY, width, height);
originX = CGRectGetMaxX(toolbarItem.frame);
@@ -136,7 +137,7 @@
selectedViewColorFrame.size.width = kSelectedViewColorDiameter;
selectedViewColorFrame.size.height = kSelectedViewColorDiameter;
selectedViewColorFrame.origin.x = kHorizontalPadding;
selectedViewColorFrame.origin.y = floor((kDescriptionContainerHeight - kSelectedViewColorDiameter) / 2.0);
selectedViewColorFrame.origin.y = FLEXFloor((kDescriptionContainerHeight - kSelectedViewColorDiameter) / 2.0);
self.selectedViewColorIndicator.frame = selectedViewColorFrame;
self.selectedViewColorIndicator.layer.cornerRadius = ceil(selectedViewColorFrame.size.height / 2.0);
@@ -15,6 +15,18 @@
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
- (BOOL)wantsWindowToBecomeKey;
// Keyboard shortcut helpers
- (void)toggleSelectTool;
- (void)toggleMoveTool;
- (void)toggleViewsTool;
- (void)toggleMenuTool;
- (void)handleDownArrowKeyPressed;
- (void)handleUpArrowKeyPressed;
- (void)handleRightArrowKeyPressed;
- (void)handleLeftArrowKeyPressed;
@end
@@ -14,6 +14,7 @@
#import "FLEXGlobalsTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXNetworkHistoryTableViewController.h"
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
FLEXExplorerModeDefault,
@@ -120,63 +121,27 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
#pragma mark - Status Bar Wrangling for iOS 7
// Try to get the preferred status bar properties from the app's root view controller (not us).
// In general, our window shouldn't be the key window when this view controller is asked about the status bar.
// However, we guard against infinite recursion and provide a reasonable default for status bar behavior in case our window is the keyWindow.
- (UIViewController *)viewControllerForStatusBarAndOrientationProperties
{
UIViewController *viewControllerToAsk = [[[UIApplication sharedApplication] keyWindow] rootViewController];
// On iPhone, modal view controllers get asked
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
while (viewControllerToAsk.presentedViewController) {
viewControllerToAsk = viewControllerToAsk.presentedViewController;
}
}
return viewControllerToAsk;
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
UIStatusBarStyle preferredStyle = UIStatusBarStyleDefault;
if (viewControllerToAsk && viewControllerToAsk != self) {
preferredStyle = [viewControllerToAsk preferredStatusBarStyle];
}
return preferredStyle;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
UIStatusBarAnimation preferredAnimation = UIStatusBarAnimationFade;
if (viewControllerToAsk && viewControllerToAsk != self) {
preferredAnimation = [viewControllerToAsk preferredStatusBarUpdateAnimation];
}
return preferredAnimation;
}
- (BOOL)prefersStatusBarHidden
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
BOOL prefersHidden = NO;
if (viewControllerToAsk && viewControllerToAsk != self) {
prefersHidden = [viewControllerToAsk prefersStatusBarHidden];
}
return prefersHidden;
}
#pragma mark - Rotation
- (NSUInteger)supportedInterfaceOrientations
- (UIViewController *)viewControllerForRotationAndOrientation
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
NSUInteger supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
UIViewController *viewController = window.rootViewController;
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
viewController = [viewController performSelector:viewControllerSelector];
#pragma clang diagnostic pop
}
return viewController;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
UIInterfaceOrientationMask supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
if (viewControllerToAsk && viewControllerToAsk != self) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
@@ -192,7 +157,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (BOOL)shouldAutorotate
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
@@ -366,9 +331,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
if (indexOfView != NSNotFound) {
UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
UIView *view = self.viewsAtTapPoint[indexOfView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
UIView *outline = self.outlineViewsForVisibleViews[key];
if (outline) {
outline.frame = [self frameInLocalCoordinatesForView:view];
}
@@ -403,21 +368,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)selectButtonTapped:(FLEXToolbarItem *)sender
{
if (self.currentMode == FLEXExplorerModeSelect) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeSelect;
}
[self toggleSelectTool];
}
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
{
NSArray *allViews = [self allViewsInHierarchy];
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
hierarchyTVC.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
[self toggleViewsTool];
}
- (NSArray *)allViewsInHierarchy
@@ -435,20 +391,23 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (NSArray *)allWindows
{
NSMutableArray *windows = [[[UIApplication sharedApplication] windows] mutableCopy];
UIWindow *statusWindow = [self statusWindow];
if (statusWindow) {
// The windows are ordered back to front, so default to inserting the status bar at the end.
// However, it there are windows at status bar level, insert the status bar before them.
NSInteger insertionIndex = [windows count];
for (UIWindow *window in windows) {
if (window.windowLevel >= UIWindowLevelStatusBar) {
insertionIndex = [windows indexOfObject:window];
break;
}
}
[windows insertObject:statusWindow atIndex:insertionIndex];
}
BOOL includeInternalWindows = YES;
BOOL onlyVisibleWindows = NO;
NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = [UIWindow class];
invocation.selector = allWindowsSelector;
[invocation setArgument:&includeInternalWindows atIndex:2];
[invocation setArgument:&onlyVisibleWindows atIndex:3];
[invocation invoke];
__unsafe_unretained NSArray *windows = nil;
[invocation getReturnValue:&windows];
return windows;
}
@@ -460,20 +419,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
{
if (self.currentMode == FLEXExplorerModeMove) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeMove;
}
[self toggleMoveTool];
}
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
{
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
globalsViewController.delegate = self;
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
[self toggleMenuTool];
}
- (void)closeButtonTapped:(FLEXToolbarItem *)sender
@@ -580,7 +531,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
// Only if we're in selection mode
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
[self updateOutlineViewsForSelectionPoint:[tapGR locationInView:nil]];
// Note that [tapGR locationInView:nil] is broken in iOS 8, so we have to do a two step conversion to window coordinates.
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
CGPoint tapPointInView = [tapGR locationInView:self.view];
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
}
}
@@ -735,8 +690,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
newSelectedViewFrame.origin.x = floor(newSelectedViewFrame.origin.x + translation.x);
newSelectedViewFrame.origin.y = floor(newSelectedViewFrame.origin.y + translation.y);
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
self.selectedView.frame = newSelectedViewFrame;
}
@@ -830,11 +785,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// 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.
// Only for iOS 7+
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
}
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
@@ -842,21 +794,137 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
{
[self.previousKeyWindow makeKeyWindow];
UIWindow *previousKeyWindow = self.previousKeyWindow;
self.previousKeyWindow = nil;
[previousKeyWindow makeKeyWindow];
[[previousKeyWindow rootViewController] setNeedsStatusBarAppearanceUpdate];
// Restore the status bar window's normal window level.
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
// Restore the stauts bar style if the app is using global status bar management.
// Only for iOS 7+
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
}
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
[self dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
return self.previousKeyWindow != nil;
}
#pragma mark - Keyboard Shortcut Helpers
- (void)toggleSelectTool
{
if (self.currentMode == FLEXExplorerModeSelect) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeSelect;
}
}
- (void)toggleMoveTool
{
if (self.currentMode == FLEXExplorerModeMove) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeMove;
}
}
- (void)toggleViewsTool
{
BOOL viewsModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
viewsModalShown = viewsModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXHierarchyTableViewController class]];
if (viewsModalShown) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
} else {
void (^presentBlock)() = ^{
NSArray *allViews = [self allViewsInHierarchy];
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
hierarchyTVC.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
};
if (self.presentedViewController) {
[self resignKeyAndDismissViewControllerAnimated:NO completion:presentBlock];
} else {
presentBlock();
}
}
}
- (void)toggleMenuTool
{
BOOL menuModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
menuModalShown = menuModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXGlobalsTableViewController class]];
if (menuModalShown) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
} else {
void (^presentBlock)() = ^{
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
globalsViewController.delegate = self;
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
};
if (self.presentedViewController) {
[self resignKeyAndDismissViewControllerAnimated:NO completion:presentBlock];
} else {
presentBlock();
}
}
}
- (void)handleDownArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
}
}
}
- (void)handleUpArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex < [self.viewsAtTapPoint count] - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
}
}
- (void)handleRightArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
}
}
- (void)handleLeftArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / [[UIScreen mainScreen] scale];
self.selectedView.frame = frame;
}
}
@end
+70
View File
@@ -0,0 +1,70 @@
//
// FLEXManager.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, readonly) BOOL isHidden;
- (void)showExplorer;
- (void)hideExplorer;
- (void)toggleExplorer;
#pragma mark - Network Debugging
/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
/// Full responses are kept temporarily in a size limited cache and may be pruged under memory pressure.
@property (nonatomic, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
#pragma mark - Keyboard Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
/// @param modifiers Modifier keys such as shift, command, or alt/option
/// @param action The block to run on the main thread when the key & modifier combination is recognized.
/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
/// @note The action block will be retained for the duration of the application. You may want to use weak references.
/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
#pragma mark - Extensions
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
/// @note This method must be called from the main thread.
/// The objectFutureBlock will be invoked from the main thread and may return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
@end
+360
View File
@@ -0,0 +1,360 @@
//
// FLEXManager.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXManager.h"
#import "FLEXExplorerViewController.h"
#import "FLEXWindow.h"
#import "FLEXGlobalsTableViewControllerEntry.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXKeyboardShortcutManager.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXKeyboardHelpViewController.h"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
@property (nonatomic, strong) FLEXWindow *explorerWindow;
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
@end
@implementation FLEXManager
+ (instancetype)sharedManager
{
static FLEXManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[[self class] alloc] init];
});
return sharedManager;
}
- (instancetype)init
{
self = [super init];
if (self) {
_userGlobalEntries = [[NSMutableArray alloc] init];
}
return self;
}
- (FLEXWindow *)explorerWindow
{
NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
if (!_explorerWindow) {
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
_explorerWindow.eventDelegate = self;
_explorerWindow.rootViewController = self.explorerViewController;
}
return _explorerWindow;
}
- (FLEXExplorerViewController *)explorerViewController
{
if (!_explorerViewController) {
_explorerViewController = [[FLEXExplorerViewController alloc] init];
_explorerViewController.delegate = self;
}
return _explorerViewController;
}
- (void)showExplorer
{
self.explorerWindow.hidden = NO;
}
- (void)hideExplorer
{
self.explorerWindow.hidden = YES;
}
- (void)toggleExplorer {
if (self.explorerWindow.isHidden) {
[self showExplorer];
} else {
[self hideExplorer];
}
}
- (BOOL)isHidden
{
return self.explorerWindow.isHidden;
}
- (BOOL)isNetworkDebuggingEnabled
{
return [FLEXNetworkObserver isEnabled];
}
- (void)setNetworkDebuggingEnabled:(BOOL)networkDebuggingEnabled
{
[FLEXNetworkObserver setEnabled:networkDebuggingEnabled];
}
- (NSUInteger)networkResponseCacheByteLimit
{
return [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
}
- (void)setNetworkResponseCacheByteLimit:(NSUInteger)networkResponseCacheByteLimit
{
[[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:networkResponseCacheByteLimit];
}
#pragma mark - FLEXWindowEventDelegate
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
{
// Ask the explorer view controller
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
}
- (BOOL)canBecomeKeyWindow
{
// Only when the explorer view controller wants it because it needs to accept key input & affect the status bar.
return [self.explorerViewController wantsWindowToBecomeKey];
}
#pragma mark - FLEXExplorerViewControllerDelegate
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
{
[self hideExplorer];
}
#pragma mark - Simulator Shortcuts
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
{
# if TARGET_OS_SIMULATOR
[[FLEXKeyboardShortcutManager sharedManager] registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description];
#endif
}
- (void)setSimulatorShortcutsEnabled:(BOOL)simulatorShortcutsEnabled
{
# if TARGET_OS_SIMULATOR
[[FLEXKeyboardShortcutManager sharedManager] setEnabled:simulatorShortcutsEnabled];
#endif
}
- (BOOL)simulatorShortcutsEnabled
{
# if TARGET_OS_SIMULATOR
return [[FLEXKeyboardShortcutManager sharedManager] isEnabled];
#else
return NO;
#endif
}
- (void)registerDefaultSimulatorShortcuts
{
[self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
[self toggleExplorer];
} description:@"Toggle FLEX toolbar"];
[self registerSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMenuTool];
} description:@"Toggle FLEX globals menu"];
[self registerSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleViewsTool];
} description:@"Toggle view hierarchy menu"];
[self registerSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleSelectTool];
} description:@"Toggle select tool"];
[self registerSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMoveTool];
} description:@"Toggle move tool"];
[self registerSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXNetworkHistoryTableViewController class]];
} description:@"Toggle network history view"];
[self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
if ([self isHidden]) {
[self tryScrollDown];
} else {
[self.explorerViewController handleDownArrowKeyPressed];
}
} description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
[self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
if ([self isHidden]) {
[self tryScrollUp];
} else {
[self.explorerViewController handleUpArrowKeyPressed];
}
} description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
[self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
if (![self isHidden]) {
[self.explorerViewController handleRightArrowKeyPressed];
}
} description:@"Move selected view right"];
[self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
if ([self isHidden]) {
[self tryGoBack];
} else {
[self.explorerViewController handleLeftArrowKeyPressed];
}
} description:@"Move selected view left"];
[self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
} description:@"Toggle (this) help menu"];
[self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
[[[self topViewController] presentingViewController] dismissViewControllerAnimated:YES completion:nil];
} description:@"End editing text\n\t\tDismiss top view controller"];
[self registerSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
[self toggleTopViewControllerOfClass:[FLEXFileBrowserTableViewController class]];
} description:@"Toggle file browser menu"];
}
+ (void)load
{
dispatch_async(dispatch_get_main_queue(), ^{
[[[self class] sharedManager] registerDefaultSimulatorShortcuts];
});
}
#pragma mark - Extensions
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
{
NSParameterAssert(entryName);
NSParameterAssert(objectFutureBlock);
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
entryName = entryName.copy;
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
return entryName;
} viewControllerFuture:^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
}];
[self.userGlobalEntries addObject:entry];
}
- (void)registerGlobalEntryWithName:(NSString *)entryName viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock
{
NSParameterAssert(entryName);
NSParameterAssert(viewControllerFutureBlock);
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
entryName = entryName.copy;
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
return entryName;
} viewControllerFuture:^UIViewController *{
UIViewController *viewController = viewControllerFutureBlock();
NSCAssert(viewController, @"'%@' entry returned nil viewController. viewControllerFutureBlock should never return nil.", entryName);
return viewController;
}];
[self.userGlobalEntries addObject:entry];
}
- (void)tryScrollDown
{
UIScrollView *firstScrollView = [self firstScrollView];
CGPoint contentOffset = [firstScrollView contentOffset];
CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
CGFloat maxContentOffsetY = firstScrollView.contentSize.height + firstScrollView.contentInset.bottom - firstScrollView.frame.size.height;
distance = MIN(maxContentOffsetY - firstScrollView.contentOffset.y, distance);
contentOffset.y += distance;
[firstScrollView setContentOffset:contentOffset animated:YES];
}
- (void)tryScrollUp
{
UIScrollView *firstScrollView = [self firstScrollView];
CGPoint contentOffset = [firstScrollView contentOffset];
CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
CGFloat minContentOffsetY = -firstScrollView.contentInset.top;
distance = MIN(firstScrollView.contentOffset.y - minContentOffsetY, distance);
contentOffset.y -= distance;
[firstScrollView setContentOffset:contentOffset animated:YES];
}
- (UIScrollView *)firstScrollView
{
NSMutableArray *views = [[[[UIApplication sharedApplication] keyWindow] subviews] mutableCopy];
UIScrollView *scrollView = nil;
while ([views count] > 0) {
UIView *view = [views firstObject];
[views removeObjectAtIndex:0];
if ([view isKindOfClass:[UIScrollView class]]) {
scrollView = (UIScrollView *)view;
break;
} else {
[views addObjectsFromArray:[view subviews]];
}
}
return scrollView;
}
- (void)tryGoBack
{
UINavigationController *navigationController = nil;
UIViewController *topViewController = [self topViewController];
if ([topViewController isKindOfClass:[UINavigationController class]]) {
navigationController = (UINavigationController *)topViewController;
} else {
navigationController = topViewController.navigationController;
}
[navigationController popViewControllerAnimated:YES];
}
- (UIViewController *)topViewController
{
UIViewController *topViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
while ([topViewController presentedViewController]) {
topViewController = [topViewController presentedViewController];
}
return topViewController;
}
- (void)toggleTopViewControllerOfClass:(Class)class
{
UIViewController *topViewController = [self topViewController];
if ([topViewController isKindOfClass:[UINavigationController class]] && [[[(UINavigationController *)topViewController viewControllers] firstObject] isKindOfClass:[class class]]) {
[[topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
} else {
id viewController = [[class alloc] init];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[topViewController presentViewController:navigationController animated:YES completion:nil];
}
}
- (void)showExplorerIfNeeded
{
if ([self isHidden]) {
[self showExplorer];
}
}
@end
@@ -115,7 +115,7 @@
titleSize = CGSizeMake(ceil(titleSize.width), ceil(titleSize.height));
titleRect.size = titleSize;
titleRect.origin.y = contentRect.origin.y + CGRectGetMaxY(contentRect) - titleSize.height;
titleRect.origin.x = contentRect.origin.x + floor((contentRect.size.width - titleSize.width) / 2.0);
titleRect.origin.x = contentRect.origin.x + FLEXFloor((contentRect.size.width - titleSize.width) / 2.0);
return titleRect;
}
@@ -124,8 +124,8 @@
CGSize imageSize = self.image.size;
CGRect titleRect = [self titleRectForContentRect:contentRect];
CGFloat availableHeight = contentRect.size.height - titleRect.size.height - [[self class] topMargin];
CGFloat originY = [[self class] topMargin] + floor((availableHeight - imageSize.height) / 2.0);
CGFloat originX = floor((contentRect.size.width - imageSize.width) / 2.0);
CGFloat originY = [[self class] topMargin] + FLEXFloor((availableHeight - imageSize.height) / 2.0);
CGFloat originX = FLEXFloor((contentRect.size.width - imageSize.width) / 2.0);
CGRect imageRect = CGRectMake(originX, originY, imageSize.width, imageSize.height);
return imageRect;
}
@@ -19,5 +19,6 @@
@protocol FLEXWindowEventDelegate <NSObject>
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
- (BOOL)canBecomeKeyWindow;
@end
+67
View File
@@ -0,0 +1,67 @@
//
// FLEXWindow.m
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXWindow.h"
#import <objc/runtime.h>
@implementation FLEXWindow
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
// UIWindowLevelStatusBar + 100 seems to hit that balance.
self.windowLevel = UIWindowLevelStatusBar + 100.0;
}
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
}
return pointInside;
}
- (BOOL)shouldAffectStatusBarAppearance
{
return [self isKeyWindow];
}
- (BOOL)canBecomeKeyWindow
{
return [self.eventDelegate canBecomeKeyWindow];
}
+ (void)initialize
{
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
// Done at runtime with an obfuscated selector because it is private API. But you shoudn't ship this to the App Store anyways...
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
IMP canAffectImplementation = method_getImplementation(shouldAffectMethod);
class_addMethod(self, canAffectSelector, canAffectImplementation, method_getTypeEncoding(shouldAffectMethod));
// One more...
NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))];
SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString);
Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow));
IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod);
class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation, method_getTypeEncoding(canBecomeKeyMethod));
}
@end
+17
View File
@@ -0,0 +1,17 @@
//
// FLEX.h
// FLEX
//
// Created by Eric Horacek on 7/18/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for FLEX.
FOUNDATION_EXPORT double FLEXVersionNumber;
//! Project version string for FLEX.
FOUNDATION_EXPORT const unsigned char FLEXVersionString[];
#import <FLEX/FLEXManager.h>
@@ -1,181 +0,0 @@
//
// FLEXFileBrowserTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
@interface FLEXFileBrowserTableViewController ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray *childPaths;
@property (nonatomic, strong) NSNumber *recursiveSize;
@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];
FLEXFileBrowserTableViewController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away.
if (!weakSelf) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
});
self.childPaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
}
return self;
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.childPaths count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString *sizeString = nil;
if (!self.recursiveSize) {
sizeString = @"Computing size…";
} else {
sizeString = [NSByteCountFormatter stringFromByteCount:[self.recursiveSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
}
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)[self.childPaths count], sizeString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
NSString *fullPath = [self.path stringByAppendingPathComponent:subpath];
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:NULL];
BOOL isDirectory = [[attributes fileType] isEqual:NSFileTypeDirectory];
NSString *subtitle = nil;
if (isDirectory) {
NSUInteger count = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:fullPath error:NULL] count];
subtitle = [NSString stringWithFormat:@"%lu file%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
} else {
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleFile];
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, [attributes fileModificationDate]];
}
static NSString *textCellIdentifier = @"textCell";
static NSString *imageCellIdentifier = @"imageCell";
UITableViewCell *cell = nil;
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
BOOL showImagePreview = [FLEXUtility isImagePathExtension:[fullPath pathExtension]];
NSString *cellIdentifier = showImagePreview ? imageCellIdentifier : textCellIdentifier;
if (!cell) {
cell = [[UITableViewCell 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 = [subpath lastPathComponent];
cell.textLabel.text = cellTitle;
cell.detailTextLabel.text = subtitle;
if (showImagePreview) {
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.imageView.image = [UIImage imageWithContentsOfFile:fullPath];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
NSString *fullPath = [self.path stringByAppendingPathComponent:subpath];
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if ([FLEXUtility isImagePathExtension:[fullPath pathExtension]]) {
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([[subpath pathExtension] isEqual:@"archive"]) {
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
} else if ([[subpath pathExtension] isEqualToString:@"json"]) {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
id jsonObject = [NSJSONSerialization JSONObjectWithData:fileData options:0 error:NULL];
prettyString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
} else if ([[subpath pathExtension] isEqualToString:@"plist"]) {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
}
if ([prettyString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:[subpath pathExtension]]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else {
NSString *fileString = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:NULL];
if ([fileString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
}
}
}
if (drillInViewController) {
drillInViewController.title = [subpath lastPathComponent];
[self.navigationController pushViewController:drillInViewController animated:YES];
} else {
[self.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];
}
}
@end
@@ -0,0 +1,26 @@
//
// PTDatabaseManager.h
// Derived from:
//
// FMDatabase.h
// FMDB( https://github.com/ccgus/fmdb )
//
// Created by Peng Tao on 15/11/23.
//
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Flying Meat Inc. licenses this file to you.
#import <Foundation/Foundation.h>
@interface FLEXDatabaseManager : NSObject
- (instancetype)initWithPath:(NSString*)aPath;
- (BOOL)open;
- (NSArray *)queryAllTables;
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName;
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName;
@end
@@ -0,0 +1,195 @@
//
// PTDatabaseManager.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXDatabaseManager.h"
#import <sqlite3.h>
static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
@implementation FLEXDatabaseManager
{
sqlite3* _db;
NSString* _databasePath;
}
- (instancetype)initWithPath:(NSString*)aPath
{
self = [super init];
if (self) {
_databasePath = [aPath copy];
}
return self;
}
- (BOOL)open {
if (_db) {
return YES;
}
int err = sqlite3_open([_databasePath UTF8String], &_db);
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
return YES;
}
- (BOOL)close {
if (!_db) {
return YES;
}
int rc;
BOOL retry;
BOOL triedFinalizingOpenStatements = NO;
do {
retry = NO;
rc = sqlite3_close(_db);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
retry = YES;
}
}
}
else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
}
}
while (retry);
_db = nil;
return YES;
}
- (NSArray *)queryAllTables
{
return [self executeQuery:QUERY_TABLENAMES_SQL];
}
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName
{
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
NSArray *resultArray = [self executeQuery:sql];
NSMutableArray *array = [NSMutableArray array];
for (NSDictionary *dict in resultArray) {
[array addObject:dict[@"name"]];
}
return array;
}
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName
{
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
return [self executeQuery:sql];
}
#pragma mark -
#pragma mark - Private
- (NSArray *)executeQuery:(NSString *)sql
{
[self open];
NSMutableArray *resultArray = [NSMutableArray array];
sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count(pstmt);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(pstmt, columnIdx)];
id objectValue = [self objectForColumnIndex:columnIdx stmt:pstmt];
[dict setObject:objectValue forKey:columnName];
}
[resultArray addObject:dict];
}
}
}
[self close];
return resultArray;
}
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
int columnType = sqlite3_column_type(stmt, columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:sqlite3_column_int64(stmt, columnIdx)];
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:sqlite3_column_double(stmt, columnIdx)];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [self dataForColumnIndex:columnIdx stmt:stmt];
}
else {
//default to a string for everything else
returnValue = [self stringForColumnIndex:columnIdx stmt:stmt];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *c = (const char *)sqlite3_column_text(stmt, columnIdx);
if (!c) {
// null row.
return nil;
}
return [NSString stringWithUTF8String:c];
}
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt{
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob(stmt, columnIdx);
int dataSize = sqlite3_column_bytes(stmt, columnIdx);
if (dataBuffer == NULL) {
return nil;
}
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
}
@end
@@ -0,0 +1,48 @@
//
// PTMultiColumnTableView.h
// PTMultiColumnTableViewDemo
//
// Created by Peng Tao on 15/11/16.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableColumnHeader.h"
@class FLEXMultiColumnTableView;
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
@required
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text;
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType;
@end
@protocol FLEXMultiColumnTableViewDataSource <NSObject>
@required
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSString *)columnNameInColumn:(NSInteger)column;
- (NSString *)rowNameInRow:(NSInteger)row;
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
- (NSArray *)contentAtRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
@end
@interface FLEXMultiColumnTableView : UIView
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource>dataSource;
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate>delegate;
- (void)reloadData;
@end
@@ -0,0 +1,341 @@
//
// PTMultiColumnTableView.m
// PTMultiColumnTableViewDemo
//
// Created by Peng Tao on 15/11/16.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXMultiColumnTableView.h"
#import "FLEXTableContentCell.h"
#import "FLEXTableLeftCell.h"
@interface FLEXMultiColumnTableView ()
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
@property (nonatomic, 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, strong) NSDictionary *sortStatusDict;
@property (nonatomic, strong) NSArray *rowData;
@end
static const CGFloat kColumnMargin = 1;
@implementation FLEXMultiColumnTableView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self loadUI];
}
return self;
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self reloadData];
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
CGFloat topheaderHeight = [self topHeaderHeight];
CGFloat leftHeaderWidth = [self leftHeaderWidth];
CGFloat contentWidth = 0.0;
NSInteger rowsCount = [self numberOfColumns];
for (int i = 0; i < rowsCount; i++) {
contentWidth += [self contentWidthForColumn:i];
}
self.leftTableView.frame = CGRectMake(0, topheaderHeight, leftHeaderWidth, height - topheaderHeight);
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, 0, 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.contentScrollView.contentSize = self.contentTableView.frame.size;
self.leftHeader.frame = CGRectMake(0, 0, [self leftHeaderWidth], [self topHeaderHeight]);
}
- (void)loadUI
{
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
- (void)reloadData
{
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
#pragma mark - UI
- (void)loadHeaderScrollView
{
UIScrollView *headerScrollView = [[UIScrollView alloc] init];
headerScrollView.delegate = self;
self.headerScrollView = headerScrollView;
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
[self addSubview:headerScrollView];
}
- (void)loadContentScrollView
{
UIScrollView *scrollView = [[UIScrollView alloc] init];
scrollView.bounces = NO;
scrollView.delegate = self;
UITableView *tableView = [[UITableView alloc] init];
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self addSubview:scrollView];
[scrollView addSubview:tableView];
self.contentScrollView = scrollView;
self.contentTableView = tableView;
}
- (void)loadLeftView
{
UITableView *leftTableView = [[UITableView alloc] init];
leftTableView.delegate = self;
leftTableView.dataSource = self;
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.leftTableView = leftTableView;
[self addSubview:leftTableView];
UIView *leftHeader = [[UIView alloc] init];
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
self.leftHeader = leftHeader;
[self addSubview:leftHeader];
}
#pragma mark - Data
- (void)loadHeaderData
{
NSArray *subviews = self.headerScrollView.subviews;
for (UIView *subview in subviews) {
[subview removeFromSuperview];
}
CGFloat x = 0.0;
CGFloat w = 0.0;
for (int i = 0; i < [self numberOfColumns] ; i++) {
w = [self contentWidthForColumn:i] + [self columnMargin];
FLEXTableColumnHeader *cell = [[FLEXTableColumnHeader alloc] initWithFrame:CGRectMake(x, 0, w, [self topHeaderHeight] - 1)];
cell.label.text = [self columnTitleForColumn:i];
[self.headerScrollView addSubview:cell];
FLEXTableColumnHeaderSortType type = [self.sortStatusDict[[self columnTitleForColumn:i]] integerValue];
[cell changeSortStatusWithType:type];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(contentHeaderTap:)];
[cell addGestureRecognizer:gesture];
cell.userInteractionEnabled = YES;
x = x + w;
}
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture
{
FLEXTableColumnHeader *header = (FLEXTableColumnHeader *)gesture.view;
NSString *string = header.label.text;
FLEXTableColumnHeaderSortType currentType = [self.sortStatusDict[string] integerValue];
FLEXTableColumnHeaderSortType newType ;
switch (currentType) {
case FLEXTableColumnHeaderSortTypeNone:
newType = FLEXTableColumnHeaderSortTypeAsc;
break;
case FLEXTableColumnHeaderSortTypeAsc:
newType = FLEXTableColumnHeaderSortTypeDesc;
break;
case FLEXTableColumnHeaderSortTypeDesc:
newType = FLEXTableColumnHeaderSortTypeAsc;
break;
}
self.sortStatusDict = @{header.label.text : @(newType)};
[header changeSortStatusWithType:newType];
[self.delegate multiColumnTableView:self didTapHeaderWithText:string sortType:newType];
}
- (void)loadContentData
{
[self.contentTableView reloadData];
}
- (void)loadLeftViewData
{
[self.leftTableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIColor *backgroundColor = [UIColor whiteColor];
if (indexPath.row % 2 != 0) {
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
}
if (tableView != self.leftTableView) {
self.rowData = [self.dataSource contentAtRow:indexPath.row];
FLEXTableContentCell *cell = [FLEXTableContentCell cellWithTableView:tableView
columnNumber:[self numberOfColumns]];
cell.contentView.backgroundColor = backgroundColor;
cell.delegate = self;
for (int i = 0 ; i < cell.labels.count; i++) {
UILabel *label = cell.labels[i];
label.textColor = [UIColor blackColor];
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
if ([content isEqualToString:@"<null>"]) {
label.textColor = [UIColor lightGrayColor];
content = @"NULL";
}
label.text = content;
label.backgroundColor = backgroundColor;
}
return cell;
}
else {
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
cell.contentView.backgroundColor = backgroundColor;
cell.titlelabel.text = [self rowTitleForRow:indexPath.row];
return cell;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.dataSource numberOfRowsInTableView:self];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == self.contentScrollView) {
self.headerScrollView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.headerScrollView) {
self.contentScrollView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.leftTableView) {
self.contentTableView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.contentTableView) {
self.leftTableView.contentOffset = scrollView.contentOffset;
}
}
#pragma mark -
#pragma mark UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (tableView == self.leftTableView) {
[self.contentTableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
else if (tableView == self.contentTableView) {
[self.leftTableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
}
#pragma mark -
#pragma mark DataSource Accessor
- (NSInteger)numberOfrows
{
return [self.dataSource numberOfRowsInTableView:self];
}
- (NSInteger)numberOfColumns
{
return [self.dataSource numberOfColumnsInTableView:self];
}
- (NSString *)columnTitleForColumn:(NSInteger)column
{
return [self.dataSource columnNameInColumn:column];
}
- (NSString *)rowTitleForRow:(NSInteger)row
{
return [self.dataSource rowNameInRow:row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
{
return [self.dataSource contentAtColumn:column row:row];
}
- (CGFloat)contentWidthForColumn:(NSInteger)column
{
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row
{
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
}
- (CGFloat)topHeaderHeight
{
return [self.dataSource heightForTopHeaderInTableView:self];
}
- (CGFloat)leftHeaderWidth
{
return [self.dataSource widthForLeftHeaderInTableView:self];
}
- (CGFloat)columnMargin
{
return kColumnMargin;
}
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text
{
[self.delegate multiColumnTableView:self didTapLabelWithText:text];
}
@end
@@ -0,0 +1,24 @@
//
// FLEXTableContentHeaderCell.h
// UICatalog
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
FLEXTableColumnHeaderSortTypeNone = 0,
FLEXTableColumnHeaderSortTypeAsc,
FLEXTableColumnHeaderSortTypeDesc,
};
@interface FLEXTableColumnHeader : UIView
@property (nonatomic, strong) UILabel *label;
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
@end
@@ -0,0 +1,60 @@
//
// FLEXTableContentHeaderCell.m
// UICatalog
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
//
#import "FLEXTableColumnHeader.h"
@implementation FLEXTableColumnHeader
{
UILabel *_arrowLabel;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
label.font = [UIFont systemFontOfSize:13.0];
[self addSubview:label];
self.label = label;
_arrowLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 20, 0, 20, frame.size.height)];
_arrowLabel.font = [UIFont systemFontOfSize:13.0];
[self addSubview:_arrowLabel];
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width - 1, 2, 1, frame.size.height - 4)];
line.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
[self addSubview:line];
}
return self;
}
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type
{
switch (type) {
case FLEXTableColumnHeaderSortTypeNone:
_arrowLabel.text = @"";
break;
case FLEXTableColumnHeaderSortTypeAsc:
_arrowLabel.text = @"⬆️";
break;
case FLEXTableColumnHeaderSortTypeDesc:
_arrowLabel.text = @"⬇️";
break;
}
}
@end
@@ -0,0 +1,27 @@
//
// FLEXTableContentCell.h
// UICatalog
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXTableContentCell;
@protocol FLEXTableContentCellDelegate <NSObject>
@optional
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text;
@end
@interface FLEXTableContentCell : UITableViewCell
@property (nonatomic, strong)NSArray *labels;
@property (nonatomic, weak) id<FLEXTableContentCellDelegate>delegate;
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
@end
@@ -0,0 +1,66 @@
//
// FLEXTableContentCell.m
// UICatalog
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import "FLEXTableContentCell.h"
#import "FLEXMultiColumnTableView.h"
@interface FLEXTableContentCell ()
@end
@implementation FLEXTableContentCell
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
{
static NSString *identifier = @"FLEXTableContentCell";
FLEXTableContentCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[FLEXTableContentCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
NSMutableArray *labels = [NSMutableArray array];
for (int i = 0; i < number ; i++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = [UIColor whiteColor];
label.font = [UIFont systemFontOfSize:13.0];
label.textAlignment = NSTextAlignmentLeft;
label.backgroundColor = [UIColor greenColor];
[labels addObject:label];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
action:@selector(labelDidTap:)];
[label addGestureRecognizer:gesture];
label.userInteractionEnabled = YES;
[cell.contentView addSubview:label];
cell.contentView.backgroundColor = [UIColor whiteColor];
}
cell.labels = labels;
}
return cell;
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat labelWidth = self.contentView.frame.size.width / self.labels.count;
CGFloat labelHeight = self.contentView.frame.size.height;
for (int i = 0; i < self.labels.count; i++) {
UILabel *label = self.labels[i];
label.frame = CGRectMake(labelWidth * i + 5, 0, (labelWidth - 10), labelHeight);
}
}
- (void)labelDidTap:(UIGestureRecognizer *)gesture
{
UILabel *label = (UILabel *)gesture.view;
if ([self.delegate respondsToSelector:@selector(tableContentCell:labelDidTapWithText:)]) {
[self.delegate tableContentCell:self labelDidTapWithText:label.text];
}
}
@end
@@ -0,0 +1,16 @@
//
// PTTableContentViewController.h
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableContentViewController : UIViewController
@property (nonatomic, strong) NSArray *columnsArray;
@property (nonatomic, strong) NSArray *contentsArray;
@end
@@ -0,0 +1,184 @@
//
// PTTableContentViewController.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015 Peng Tao. All rights reserved.
//
#import "FLEXTableContentViewController.h"
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
@property (nonatomic, strong)FLEXMultiColumnTableView *multiColumView;
@end
@implementation FLEXTableContentViewController
- (instancetype)init
{
self = [super init];
if (self) {
CGRect rectStatus = [UIApplication sharedApplication].statusBarFrame;
CGFloat y = 64;
if (rectStatus.size.height == 0) {
y = 32;
}
_multiColumView = [[FLEXMultiColumnTableView alloc] initWithFrame:
CGRectMake(0, y, self.view.frame.size.width, self.view.frame.size.height - y)];
_multiColumView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_multiColumView.backgroundColor = [UIColor whiteColor];
_multiColumView.dataSource = self;
_multiColumView.delegate = self;
self.automaticallyAdjustsScrollViewInsets = NO;
[self.view addSubview:_multiColumView];
}
return self;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.multiColumView reloadData];
}
#pragma mark -
#pragma mark MultiColumnTableView DataSource
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView
{
return self.columnsArray.count;
}
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView
{
return self.contentsArray.count;
}
- (NSString *)columnNameInColumn:(NSInteger)column
{
return self.columnsArray[column];
}
- (NSString *)rowNameInRow:(NSInteger)row
{
return [NSString stringWithFormat:@"%ld",(long)row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row
{
if (self.contentsArray.count > row) {
NSDictionary *dic = self.contentsArray[row];
if (self.contentsArray.count > column) {
return [NSString stringWithFormat:@"%@",[dic objectForKey:self.columnsArray[column]]];
}
}
return @"";
}
- (NSArray *)contentAtRow:(NSInteger)row
{
NSMutableArray *result = [NSMutableArray array];
if (self.contentsArray.count > row) {
NSDictionary *dic = self.contentsArray[row];
for (int i = 0; i < self.columnsArray.count; i ++) {
[result addObject:dic[self.columnsArray[i]]];
}
return result;
}
return nil;
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
heightForContentCellInRow:(NSInteger)row
{
return 40;
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
widthForContentCellInColumn:(NSInteger)column
{
return 120;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView
{
return 40;
}
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView
{
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.contentsArray.count];
NSDictionary *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attrs context:nil].size;
return size.width + 20;
}
#pragma mark -
#pragma mark MultiColumnTableView Delegate
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text
{
FLEXWebViewController * detailViewController = [[FLEXWebViewController alloc] initWithText:text];
[self.navigationController pushViewController:detailViewController animated:YES];
}
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType
{
NSArray *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if ([obj1 objectForKey:text] == [NSNull null]) {
return NSOrderedAscending;
}
if ([obj2 objectForKey:text] == [NSNull null]) {
return NSOrderedDescending;
}
NSComparisonResult result = [[obj1 objectForKey:text] compare:[obj2 objectForKey:text]];
return result;
}];
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
NSEnumerator *contentReverseEvumerator = [sortContentData reverseObjectEnumerator];
sortContentData = [NSArray arrayWithArray:[contentReverseEvumerator allObjects]];
}
self.contentsArray = sortContentData;
[self.multiColumView reloadData];
}
#pragma mark -
#pragma mark About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super willTransitionToTraitCollection:newCollection
withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
_multiColumView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
}
else {
_multiColumView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
}
[self.view setNeedsLayout];
} completion:nil];
}
@end
@@ -0,0 +1,17 @@
//
// FLEXTableLeftCell.h
// UICatalog
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableLeftCell : UITableViewCell
@property (nonatomic, strong) UILabel *titlelabel;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@end
@@ -0,0 +1,35 @@
//
// FLEXTableLeftCell.m
// UICatalog
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015 f. All rights reserved.
//
#import "FLEXTableLeftCell.h"
@implementation FLEXTableLeftCell
+ (instancetype)cellWithTableView:(UITableView *)tableView
{
static NSString *identifier = @"FLEXTableLeftCell";
FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[FLEXTableLeftCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
textLabel.textAlignment = NSTextAlignmentCenter;
textLabel.font = [UIFont systemFontOfSize:13.0];
textLabel.backgroundColor = [UIColor clearColor];
[cell.contentView addSubview:textLabel];
cell.titlelabel = textLabel;
}
return cell;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.titlelabel.frame = self.contentView.frame;
}
@end
@@ -0,0 +1,15 @@
//
// PTTableListViewController.h
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableListViewController : UITableViewController
- (instancetype)initWithPath:(NSString *)path;
@end
@@ -0,0 +1,82 @@
//
// PTTableListViewController.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015 Peng Tao. All rights reserved.
//
#import "FLEXTableListViewController.h"
#import "FLEXDatabaseManager.h"
#import "FLEXTableContentViewController.h"
@interface FLEXTableListViewController ()
{
FLEXDatabaseManager *_dbm;
NSString *_databasePath;
}
@property (nonatomic, strong) NSArray *tables;
@end
@implementation FLEXTableListViewController
- (instancetype)initWithPath:(NSString *)path
{
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_databasePath = [path copy];
_dbm = [[FLEXDatabaseManager alloc] initWithPath:path];
[_dbm open];
[self getAllTables];
}
return self;
}
- (void)getAllTables
{
NSArray *resultArray = [_dbm queryAllTables];
NSMutableArray *array = [NSMutableArray array];
for (NSDictionary *dict in resultArray) {
[array addObject:dict[@"name"]];
}
self.tables = array;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tables.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FLEXTableListViewControllerCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"FLEXTableListViewControllerCell"];
}
cell.textLabel.text = self.tables[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXTableContentViewController *contentViewController = [[FLEXTableContentViewController alloc] init];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.tables[indexPath.row]];
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.tables[indexPath.row]];
contentViewController.title = self.tables[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];
}
@end
@@ -0,0 +1,21 @@
FMDB
Copyright (c) 2008-2014 Flying Meat Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -87,6 +87,11 @@
[self.tableView reloadData];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Dismiss the keyboard when interacting with filtered results.
@@ -126,7 +131,7 @@
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
NSString *className = self.filteredClassNames[indexPath.row];
Class selectedClass = objc_getClass([className UTF8String]);
FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
[self.navigationController pushViewController:objectExplorer animated:YES];
@@ -0,0 +1,13 @@
//
// FLEXCookiesTableViewController.h
// FLEX
//
// Created by Rich Robinson on 19/10/2015.
// Copyright © 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXCookiesTableViewController : UITableViewController
@end
@@ -0,0 +1,71 @@
//
// FLEXCookiesTableViewController.m
// FLEX
//
// Created by Rich Robinson on 19/10/2015.
// Copyright © 2015 Flipboard. All rights reserved.
//
#import "FLEXCookiesTableViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXUtility.h"
@interface FLEXCookiesTableViewController ()
@property (nonatomic, strong) NSArray *cookies;
@end
@implementation FLEXCookiesTableViewController
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
self.title = @"Cookies";
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)];
_cookies =[[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies sortedArrayUsingDescriptors:@[nameSortDescriptor]];
}
return self;
}
- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.cookies[indexPath.row];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.cookies.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%@)", cookie.name, cookie.value];
cell.detailTextLabel.text = cookie.domain;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
[self.navigationController pushViewController:cookieViewController animated:YES];
}
@end
@@ -0,0 +1,33 @@
//
// 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
@@ -0,0 +1,142 @@
//
// 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
@@ -0,0 +1,25 @@
//
// FLEXFileBrowserSearchOperation.h
// UICatalog
//
// Created by 啟倫 陳 on 2014/8/4.
// Copyright (c) 2014年 f. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol FLEXFileBrowserSearchOperationDelegate;
@interface FLEXFileBrowserSearchOperation : NSOperation
@property (nonatomic, weak) id<FLEXFileBrowserSearchOperationDelegate> delegate;
- (id)initWithPath:(NSString *)currentPath searchString:(NSString *)searchString;
@end
@protocol FLEXFileBrowserSearchOperationDelegate <NSObject>
- (void)fileBrowserSearchOperationResult:(NSArray *)searchResult size:(uint64_t)size;
@end
@@ -0,0 +1,123 @@
//
// FLEXFileBrowserSearchOperation.m
// UICatalog
//
// Created by on 2014/8/4.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXFileBrowserSearchOperation.h"
@implementation NSMutableArray (FLEXStack)
- (void)flex_push:(id)anObject
{
[self addObject:anObject];
}
- (id)flex_pop
{
id anObject = [self lastObject];
[self removeLastObject];
return anObject;
}
@end
@interface FLEXFileBrowserSearchOperation ()
@property (nonatomic, strong) NSString *path;
@property (nonatomic, strong) NSString *searchString;
@end
@implementation FLEXFileBrowserSearchOperation
#pragma mark - private
- (uint64_t)totalSizeAtPath:(NSString *)path
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
}
return totalSize;
}
#pragma mark - instance method
- (id)initWithPath:(NSString *)currentPath searchString:(NSString *)searchString
{
self = [super init];
if (self) {
self.path = currentPath;
self.searchString = searchString;
}
return self;
}
#pragma mark - methods to override
- (void)main
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSMutableArray *searchPaths = [NSMutableArray array];
NSMutableDictionary *sizeMapping = [NSMutableDictionary dictionary];
uint64_t totalSize = 0;
NSMutableArray *stack = [NSMutableArray array];
[stack flex_push:self.path];
//recursive found all match searchString paths, and precomputing there size
while ([stack count]) {
NSString *currentPath = [stack flex_pop];
NSArray *directoryPath = [fileManager contentsOfDirectoryAtPath:currentPath error:nil];
for (NSString *subPath in directoryPath) {
NSString *fullPath = [currentPath stringByAppendingPathComponent:subPath];
if ([[subPath lowercaseString] rangeOfString:[self.searchString lowercaseString]].location != NSNotFound) {
[searchPaths addObject:fullPath];
if (!sizeMapping[fullPath]) {
uint64_t fullPathSize = [self totalSizeAtPath:fullPath];
totalSize += fullPathSize;
[sizeMapping setObject:@(fullPathSize) forKey:fullPath];
}
}
BOOL isDirectory;
if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && isDirectory) {
[stack flex_push:fullPath];
}
if ([self isCancelled]) {
return;
}
}
}
//sort
NSArray *sortedArray = [searchPaths sortedArrayUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
uint64_t pathSize1 = [sizeMapping[path1] unsignedLongLongValue];
uint64_t pathSize2 = [sizeMapping[path2] unsignedLongLongValue];
if (pathSize1 < pathSize2) {
return NSOrderedAscending;
} else if (pathSize1 > pathSize2) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];
if ([self isCancelled]) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate fileBrowserSearchOperationResult:sortedArray size:totalSize];
});
}
@end
@@ -8,6 +8,8 @@
#import <UIKit/UIKit.h>
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserTableViewController : UITableViewController
- (id)initWithPath:(NSString *)path;
@@ -0,0 +1,350 @@
//
// 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 *childPaths;
@property (nonatomic, strong) NSArray *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 *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away.
if (!weakSelf) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
});
[self reloadChildPaths];
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIMenuItem *renameMenuItem = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *deleteMenuItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
[UIMenuController sharedMenuController].menuItems = @[renameMenuItem, deleteMenuItem];
}
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray *)searchResult size:(uint64_t)size
{
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.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 *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 *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];
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if ([FLEXUtility isImagePathExtension:[fullPath pathExtension]]) {
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([[subpath pathExtension] isEqual:@"archive"]) {
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
} else if ([[subpath pathExtension] isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
} else if ([[subpath pathExtension] isEqualToString:@"plist"]) {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
}
if ([prettyString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:[subpath pathExtension]]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else if ([[subpath pathExtension] isEqualToString:@"db"]) {
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
{
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(fileBrowserDelete:) || action == @selector(fileBrowserRename:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
// Empty, but has to exist for the menu to show
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
#pragma mark - FLEXFileBrowserFileOperationControllerDelegate
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller
{
[self reloadDisplayedPaths];
}
- (void)openFileController:(NSString *)fullPath
{
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [[NSURL alloc] initFileURLWithPath:fullPath];
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
self.documentController = controller;
}
- (void)fileBrowserRename:(UITableViewCell *)sender
{
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)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self reloadSearchPaths];
} else {
[self reloadChildPaths];
}
[self.tableView reloadData];
}
- (void)reloadChildPaths
{
NSMutableArray *childPaths = [NSMutableArray array];
NSArray *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];
}
@end
@@ -14,14 +14,20 @@
#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,
@@ -158,6 +164,15 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
};
break;
case FLEXGlobalsCookies:
titleFuture = ^NSString *{
return @"🍪 Cookies";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXCookiesTableViewController alloc] init];
};
break;
case FLEXGlobalsRowFileBrowser:
titleFuture = ^NSString *{
return @"📁 File Browser";
@@ -166,6 +181,24 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
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;
}
@@ -183,7 +216,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
{
self = [super initWithStyle:style];
if (self) {
self.title = @"🌎 Global State";
self.title = @"💪 FLEX";
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
}
return self;
@@ -252,7 +285,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
@@ -100,10 +100,10 @@
cell.detailTextLabel.textColor = [UIColor grayColor];
}
id instance = [self.instances objectAtIndex:indexPath.row];
id instance = self.instances[indexPath.row];
NSString *title = nil;
if ((NSInteger)[self.fieldNames count] > indexPath.row) {
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), [self.fieldNames objectAtIndex:indexPath.row]];
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), self.fieldNames[indexPath.row]];
} else {
title = [NSString stringWithFormat:@"%@ %p", NSStringFromClass(object_getClass(instance)), instance];
}
@@ -118,7 +118,7 @@
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
id instance = [self.instances objectAtIndex:indexPath.row];
id instance = self.instances[indexPath.row];
FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
[self.navigationController pushViewController:drillInViewController animated:YES];
}
@@ -112,6 +112,11 @@
[self.tableView reloadData];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
#pragma mark - Table View Data Source
@@ -57,28 +57,28 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
// While it might be a little cleaner to populate an NSMutableDictionary with class name string keys to NSNumber counts,
// we choose the CF/primitives approach because it lets us enumerate the objects in the heap without allocating any memory during enumeration.
// The alternative of creating one NSString/NSNumber per object on the heap ends up polluting the count of live objects quite a bit.
unsigned int count = 0;
Class *classes = objc_copyClassList(&count);
CFMutableDictionaryRef mutableCountsForClasses = CFDictionaryCreateMutable(NULL, count, NULL, NULL);
for (unsigned int i = 0; i < count; i++) {
unsigned int classCount = 0;
Class *classes = objc_copyClassList(&classCount);
CFMutableDictionaryRef mutableCountsForClasses = CFDictionaryCreateMutable(NULL, classCount, NULL, NULL);
for (unsigned int i = 0; i < classCount; i++) {
CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)classes[i], (const void *)0);
}
// Enumerate all objects on the heap to build the counts of instances for each class.
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
NSUInteger count = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)actualClass);
count++;
CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)actualClass, (const void *)count);
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)actualClass);
instanceCount++;
CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)actualClass, (const void *)instanceCount);
}];
// Convert our CF primitive dictionary into a nicer mapping of class name strings to counts that we will use as the table's model.
NSMutableDictionary *mutableCountsForClassNames = [NSMutableDictionary dictionary];
for (unsigned int i = 0; i < count; i++) {
for (unsigned int i = 0; i < classCount; i++) {
Class class = classes[i];
NSUInteger count = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
if (count > 0) {
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
if (instanceCount > 0) {
NSString *className = @(class_getName(class));
[mutableCountsForClassNames setObject:@(count) forKey:className];
[mutableCountsForClassNames setObject:@(instanceCount) forKey:className];
}
}
free(classes);
@@ -100,11 +100,11 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
NSUInteger totalCount = 0;
for (NSString *className in self.allClassNames) {
totalCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
totalCount += [self.instanceCountsForClassNames[className] unsignedIntegerValue];
}
NSUInteger filteredCount = 0;
for (NSString *className in self.filteredClassNames) {
filteredCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
filteredCount += [self.instanceCountsForClassNames[className] unsignedIntegerValue];
}
if (filteredCount == totalCount) {
@@ -125,6 +125,11 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
[self updateTableDataForSearchFilter];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
{
[self updateTableDataForSearchFilter];
@@ -149,8 +154,8 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortByCountIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = [self.instanceCountsForClassNames objectForKey:className1];
NSNumber *count2 = [self.instanceCountsForClassNames objectForKey:className2];
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
// Reversed for descending counts.
return [count2 compare:count1];
}];
@@ -184,7 +189,7 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
}
NSString *className = self.filteredClassNames[indexPath.row];
NSNumber *count = [self.instanceCountsForClassNames objectForKey:className];
NSNumber *count = self.instanceCountsForClassNames[className];
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld)", className, (long)[count integerValue]];
return cell;
@@ -195,7 +200,7 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
NSString *className = self.filteredClassNames[indexPath.row];
FLEXInstancesTableViewController *instancesViewController = [FLEXInstancesTableViewController instancesTableViewControllerForClassName:className];
[self.navigationController pushViewController:instancesViewController animated:YES];
}
@@ -35,7 +35,7 @@
self = [self initWithNibName:nil bundle:nil];
if (self) {
self.originalText = text;
NSString *htmlString = [NSString stringWithFormat:@"<pre>%@</pre>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
[self.webView loadHTMLString:htmlString baseURL:nil];
}
return self;
@@ -51,6 +51,14 @@
return self;
}
- (void)dealloc
{
// UIWebView's delegate is assign so we need to clear it manually.
if (_webView.delegate == self) {
_webView.delegate = nil;
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
@@ -82,6 +90,7 @@
// 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.
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
webVC.title = [[request URL] absoluteString];
[self.navigationController pushViewController:webVC animated:YES];
}
return shouldStart;
@@ -0,0 +1,21 @@
//
// FLEXSystemLogMessage.h
// UICatalog
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <asl.h>
@interface FLEXSystemLogMessage : NSObject
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, copy) NSString *sender;
@property (nonatomic, copy) NSString *messageText;
@property (nonatomic, assign) long long messageID;
@end
@@ -0,0 +1,55 @@
//
// FLEXSystemLogMessage.m
// UICatalog
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXSystemLogMessage.h"
@implementation FLEXSystemLogMessage
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
{
FLEXSystemLogMessage *logMessage = [[FLEXSystemLogMessage alloc] init];
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
if (timestamp) {
NSTimeInterval timeInterval = [@(timestamp) integerValue];
const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
if (nanoseconds) {
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
}
logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
}
const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
if (sender) {
logMessage.sender = @(sender);
}
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
if (messageText) {
logMessage.messageText = @(messageText);
}
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
if (messageID) {
logMessage.messageID = [@(messageID) longLongValue];
}
return logMessage;
}
- (BOOL)isEqual:(id)object
{
return [object isKindOfClass:[FLEXSystemLogMessage class]] && self.messageID == [object messageID];
}
- (NSUInteger)hash
{
return (NSUInteger)self.messageID;
}
@end
@@ -0,0 +1,23 @@
//
// FLEXSystemLogTableViewCell.h
// UICatalog
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXSystemLogMessage;
extern NSString *const kFLEXSystemLogTableViewCellIdentifier;
@interface FLEXSystemLogTableViewCell : UITableViewCell
@property (nonatomic, strong) FLEXSystemLogMessage *logMessage;
@property (nonatomic, copy) NSString *highlightedText;
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage;
+ (CGFloat)preferredHeightForLogMessage:(FLEXSystemLogMessage *)logMessage inWidth:(CGFloat)width;
@end
@@ -0,0 +1,128 @@
//
// FLEXSystemLogTableViewCell.m
// UICatalog
//
// Created by Ryan Olson on 1/25/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXSystemLogTableViewCell.h"
#import "FLEXSystemLogMessage.h"
NSString *const kFLEXSystemLogTableViewCellIdentifier = @"FLEXSystemLogTableViewCellIdentifier";
@interface FLEXSystemLogTableViewCell ()
@property (nonatomic, strong) UILabel *logMessageLabel;
@property (nonatomic, strong) NSAttributedString *logMessageAttributedText;
@end
@implementation FLEXSystemLogTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.logMessageLabel = [[UILabel alloc] init];
self.logMessageLabel.numberOfLines = 0;
self.separatorInset = UIEdgeInsetsZero;
self.selectionStyle = UITableViewCellSelectionStyleNone;
[self.contentView addSubview:self.logMessageLabel];
}
return self;
}
- (void)setLogMessage:(FLEXSystemLogMessage *)logMessage
{
if (![_logMessage isEqual:logMessage]) {
_logMessage = logMessage;
self.logMessageAttributedText = nil;
[self setNeedsLayout];
}
}
- (void)setHighlightedText:(NSString *)highlightedText
{
if (![_highlightedText isEqual:highlightedText]) {
_highlightedText = highlightedText;
self.logMessageAttributedText = nil;
[self setNeedsLayout];
}
}
- (NSAttributedString *)logMessageAttributedText
{
if (!_logMessageAttributedText) {
_logMessageAttributedText = [[self class] attributedTextForLogMessage:self.logMessage highlightedText:self.highlightedText];
}
return _logMessageAttributedText;
}
static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
- (void)layoutSubviews
{
[super layoutSubviews];
self.logMessageLabel.attributedText = self.logMessageAttributedText;
self.logMessageLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, kFLEXLogMessageCellInsets);
}
#pragma mark - Stateless helpers
+ (NSAttributedString *)attributedTextForLogMessage:(FLEXSystemLogMessage *)logMessage highlightedText:(NSString *)highlightedText
{
NSString *text = [self displayedTextForLogMessage:logMessage];
NSDictionary *attributes = @{ NSFontAttributeName : [UIFont fontWithName:@"CourierNewPSMT" size:12.0] };
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes];
if ([highlightedText length] > 0) {
NSMutableAttributedString *mutableAttributedText = [attributedText mutableCopy];
NSMutableDictionary *highlightAttributes = [@{ NSBackgroundColorAttributeName : [UIColor yellowColor] } mutableCopy];
[highlightAttributes addEntriesFromDictionary:attributes];
NSRange remainingSearchRange = NSMakeRange(0, text.length);
while (remainingSearchRange.location < text.length) {
remainingSearchRange.length = text.length - remainingSearchRange.location;
NSRange foundRange = [text rangeOfString:highlightedText options:NSCaseInsensitiveSearch range:remainingSearchRange];
if (foundRange.location != NSNotFound) {
remainingSearchRange.location = foundRange.location + foundRange.length;
[mutableAttributedText setAttributes:highlightAttributes range:foundRange];
} else {
break;
}
}
attributedText = mutableAttributedText;
}
return attributedText;
}
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage
{
return [NSString stringWithFormat:@"%@: %@", [self logTimeStringFromDate:logMessage.date], logMessage.messageText];
}
+ (CGFloat)preferredHeightForLogMessage:(FLEXSystemLogMessage *)logMessage inWidth:(CGFloat)width
{
UIEdgeInsets insets = kFLEXLogMessageCellInsets;
CGFloat availableWidth = width - insets.left - insets.right;
NSAttributedString *attributedLogText = [self attributedTextForLogMessage:logMessage highlightedText:nil];
CGSize labelSize = [attributedLogText boundingRectWithSize:CGSizeMake(availableWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading context:nil].size;
return labelSize.height + insets.top + insets.bottom;
}
+ (NSString *)logTimeStringFromDate:(NSDate *)date
{
static NSDateFormatter *formatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
});
return [formatter stringFromDate:date];
}
@end
@@ -0,0 +1,13 @@
//
// FLEXSystemLogTableViewController.h
// UICatalog
//
// Created by Ryan Olson on 1/19/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXSystemLogTableViewController : UITableViewController
@end
@@ -0,0 +1,209 @@
//
// FLEXSystemLogTableViewController.m
// UICatalog
//
// Created by Ryan Olson on 1/19/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXSystemLogTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXSystemLogMessage.h"
#import "FLEXSystemLogTableViewCell.h"
#import <asl.h>
@interface FLEXSystemLogTableViewController () <UISearchResultsUpdating, UISearchControllerDelegate>
@property (nonatomic, strong) UISearchController *searchController;
@property (nonatomic, copy) NSArray *logMessages;
@property (nonatomic, copy) NSArray *filteredLogMessages;
@property (nonatomic, strong) NSTimer *logUpdateTimer;
@end
@implementation FLEXSystemLogTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[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)];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.delegate = self;
self.searchController.searchResultsUpdater = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.tableView.tableHeaderView = self.searchController.searchBar;
[self updateLogMessages];
}
- (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 *logMessages = [[self class] allLogMessagesForCurrentProcess];
dispatch_async(dispatch_get_main_queue(), ^{
self.title = @"System Log";
self.logMessages = logMessages;
// "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)scrollToLastRow
{
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
if (numberOfRows > 0) {
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
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;
if (indexPath.row % 2 == 0) {
cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
} else {
cell.backgroundColor = [UIColor whiteColor];
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXSystemLogMessage *logMessage = [self logMessageAtIndexPath:indexPath];
return [FLEXSystemLogTableViewCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width];
}
#pragma mark - Copy on long press
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(copy:);
}
- (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];
}
}
- (FLEXSystemLogMessage *)logMessageAtIndexPath:(NSIndexPath *)indexPath
{
return self.searchController.isActive ? self.filteredLogMessages[indexPath.row] : self.logMessages[indexPath.row];
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchString = searchController.searchBar.text;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *filteredLogMessages = [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary *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
// Due to a mistake in asl.h, things get a little messy. We need to mark these symbols as weak since they won't exist on iOS 7 despite the compiler thinking otherwise.
// asl.h in the iOS 8.1 SDK claims that asl_next() and asl_release() were introduced in iOS 7 to replace aslresponse_next() and aslresponse_free(). However, they were actually added in iOS 8.0.
extern aslmsg asl_next(asl_object_t obj) __attribute__((weak_import));
extern void asl_release(asl_object_t obj) __attribute__((weak_import));
+ (NSArray *)allLogMessagesForCurrentProcess
{
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.
NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
aslresponse response = asl_search(NULL, query);
aslmsg aslMessage = NULL;
NSMutableArray *logMessages = [NSMutableArray array];
if (&asl_next != NULL && &asl_release != NULL) {
while ((aslMessage = asl_next(response))) {
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
asl_release(response);
} else {
// Mute incorrect deprecated warnings. We'll need the "deprecated" functions on iOS 7, where their replacements don't yet exist.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
while ((aslMessage = aslresponse_next(response))) {
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
}
aslresponse_free(response);
#pragma clang diagnostic pop
}
return logMessages;
}
@end
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
@@ -0,0 +1,13 @@
//
// FLEXNetworkHistoryTableViewController.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXNetworkHistoryTableViewController : UITableViewController
@end
@@ -0,0 +1,360 @@
//
// FLEXNetworkHistoryTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXNetworkTransactionTableViewCell.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkTransactionDetailTableViewController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkSettingsTableViewController.h"
@interface FLEXNetworkHistoryTableViewController () <UISearchResultsUpdating, UISearchControllerDelegate>
/// Backing model
@property (nonatomic, copy) NSArray *networkTransactions;
@property (nonatomic, assign) long long bytesReceived;
@property (nonatomic, copy) NSArray *filteredNetworkTransactions;
@property (nonatomic, assign) long long filteredBytesReceived;
@property (nonatomic, assign) BOOL rowInsertInProgress;
@property (nonatomic, assign) BOOL isPresentingSearch;
@property (nonatomic, strong) UISearchController *searchController;
@end
@implementation FLEXNetworkHistoryTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNewTransactionRecordedNotification:) name:kFLEXNetworkRecorderNewTransactionNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionUpdatedNotification:) name:kFLEXNetworkRecorderTransactionUpdatedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionsClearedNotification:) name:kFLEXNetworkRecorderTransactionsClearedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNetworkObserverEnabledStateChangedNotification:) name:kFLEXNetworkObserverEnabledStateChangedNotification object:nil];
self.title = @"📡 Network";
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Settings" style:UIBarButtonItemStylePlain target:self action:@selector(settingsButtonTapped:)];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[FLEXNetworkTransactionTableViewCell class] forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.rowHeight = [FLEXNetworkTransactionTableViewCell preferredCellHeight];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.delegate = self;
self.searchController.searchResultsUpdater = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.tableView.tableHeaderView = self.searchController.searchBar;
[self updateTransactions];
}
- (void)settingsButtonTapped:(id)sender
{
FLEXNetworkSettingsTableViewController *settingsViewController = [[FLEXNetworkSettingsTableViewController alloc] init];
settingsViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(settingsViewControllerDoneTapped:)];
settingsViewController.title = @"Network Debugging Settings";
UINavigationController *wrapperNavigationController = [[UINavigationController alloc] initWithRootViewController:settingsViewController];
[self presentViewController:wrapperNavigationController animated:YES completion:nil];
}
- (void)settingsViewControllerDoneTapped:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)updateTransactions
{
self.networkTransactions = [[FLEXNetworkRecorder defaultRecorder] networkTransactions];
}
- (void)setNetworkTransactions:(NSArray *)networkTransactions
{
if (![_networkTransactions isEqual:networkTransactions]) {
_networkTransactions = networkTransactions;
[self updateBytesReceived];
[self updateFilteredBytesReceived];
}
}
- (void)updateBytesReceived
{
long long bytesReceived = 0;
for (FLEXNetworkTransaction *transaction in self.networkTransactions) {
bytesReceived += transaction.receivedDataLength;
}
self.bytesReceived = bytesReceived;
[self updateFirstSectionHeader];
}
- (void)setFilteredNetworkTransactions:(NSArray *)filteredNetworkTransactions
{
if (![_filteredNetworkTransactions isEqual:filteredNetworkTransactions]) {
_filteredNetworkTransactions = filteredNetworkTransactions;
[self updateFilteredBytesReceived];
}
}
- (void)updateFilteredBytesReceived
{
long long filteredBytesReceived = 0;
for (FLEXNetworkTransaction *transaction in self.filteredNetworkTransactions) {
filteredBytesReceived += transaction.receivedDataLength;
}
self.filteredBytesReceived = filteredBytesReceived;
[self updateFirstSectionHeader];
}
- (void)updateFirstSectionHeader
{
UIView *view = [self.tableView headerViewForSection:0];
if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.textLabel.text = [self headerText];
[headerView setNeedsLayout];
}
}
- (NSString *)headerText
{
NSString *headerText = nil;
if ([FLEXNetworkObserver isEnabled]) {
long long bytesReceived = 0;
NSInteger totalRequests = 0;
if (self.searchController.isActive) {
bytesReceived = self.filteredBytesReceived;
totalRequests = [self.filteredNetworkTransactions count];
} else {
bytesReceived = self.bytesReceived;
totalRequests = [self.networkTransactions count];
}
NSString *byteCountText = [NSByteCountFormatter stringFromByteCount:bytesReceived countStyle:NSByteCountFormatterCountStyleBinary];
NSString *requestsText = totalRequests == 1 ? @"Request" : @"Requests";
headerText = [NSString stringWithFormat:@"%ld %@ (%@ received)", (long)totalRequests, requestsText, byteCountText];
} else {
headerText = @"⚠️ Debugging Disabled (Enable in Settings)";
}
return headerText;
}
#pragma mark - Notification Handlers
- (void)handleNewTransactionRecordedNotification:(NSNotification *)notification
{
[self tryUpdateTransactions];
}
- (void)tryUpdateTransactions
{
// Let the previous row insert animation finish before starting a new one to avoid stomping.
// We'll try calling the method again when the insertion completes, and we properly no-op if there haven't been changes.
if (self.rowInsertInProgress) {
return;
}
if (self.searchController.isActive) {
[self updateTransactions];
[self updateSearchResults];
return;
}
NSInteger existingRowCount = [self.networkTransactions count];
[self updateTransactions];
NSInteger newRowCount = [self.networkTransactions count];
NSInteger addedRowCount = newRowCount - existingRowCount;
if (addedRowCount != 0 && !self.isPresentingSearch) {
// Insert animation if we're at the top.
if (self.tableView.contentOffset.y <= 0.0 && addedRowCount > 0) {
[CATransaction begin];
self.rowInsertInProgress = YES;
[CATransaction setCompletionBlock:^{
self.rowInsertInProgress = NO;
[self tryUpdateTransactions];
}];
NSMutableArray *indexPathsToReload = [NSMutableArray array];
for (NSInteger row = 0; row < addedRowCount; row++) {
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
}
[self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[CATransaction commit];
} else {
// Maintain the user's position if they've scrolled down.
CGSize existingContentSize = self.tableView.contentSize;
[self.tableView reloadData];
CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
}
}
}
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
{
[self updateBytesReceived];
[self updateFilteredBytesReceived];
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
// Update both the main table view and search table view if needed.
for (FLEXNetworkTransactionTableViewCell *cell in [self.tableView visibleCells]) {
if ([cell.transaction isEqual:transaction]) {
// Using -[UITableView reloadRowsAtIndexPaths:withRowAnimation:] is overkill here and kicks off a lot of
// work that can make the table view somewhat unresponseive when lots of updates are streaming in.
// We just need to tell the cell that it needs to re-layout.
[cell setNeedsLayout];
break;
}
}
[self updateFirstSectionHeader];
}
- (void)handleTransactionsClearedNotification:(NSNotification *)notification
{
[self updateTransactions];
[self.tableView reloadData];
}
- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification
{
// Update the header, which displays a warning when network debugging is disabled
[self updateFirstSectionHeader];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.searchController.isActive ? [self.filteredNetworkTransactions count] : [self.networkTransactions count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [self headerText];
}
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14.0];
headerView.textLabel.textColor = [UIColor whiteColor];
headerView.contentView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.0];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkTransactionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXNetworkTransactionCellIdentifier forIndexPath:indexPath];
cell.transaction = [self transactionAtIndexPath:indexPath inTableView:tableView];
// Since we insert from the top, assign background colors bottom up to keep them consistent for each transaction.
NSInteger totalRows = [tableView numberOfRowsInSection:indexPath.section];
if ((totalRows - indexPath.row) % 2 == 0) {
cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
} else {
cell.backgroundColor = [UIColor whiteColor];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkTransactionDetailTableViewController *detailViewController = [[FLEXNetworkTransactionDetailTableViewController alloc] init];
detailViewController.transaction = [self transactionAtIndexPath:indexPath inTableView:tableView];
[self.navigationController pushViewController:detailViewController animated:YES];
}
#pragma mark - Menu Actions
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(copy:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
FLEXNetworkTransaction *transaction = [self transactionAtIndexPath:indexPath inTableView:tableView];
NSString *requestURLString = transaction.request.URL.absoluteString ?: @"";
[[UIPasteboard generalPasteboard] setString:requestURLString];
}
}
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath inTableView:(UITableView *)tableView
{
return self.searchController.isActive ? self.filteredNetworkTransactions[indexPath.row] : self.networkTransactions[indexPath.row];
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
[self updateSearchResults];
}
- (void)updateSearchResults
{
NSString *searchString = self.searchController.searchBar.text;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *filteredNetworkTransactions = [self.networkTransactions filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXNetworkTransaction *transaction, NSDictionary *bindings) {
return [[transaction.request.URL absoluteString] rangeOfString:searchString options:NSCaseInsensitiveSearch].length > 0;
}]];
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.searchController.searchBar.text isEqual:searchString]) {
self.filteredNetworkTransactions = filteredNetworkTransactions;
[self.tableView reloadData];
}
});
});
}
#pragma mark - UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController *)searchController
{
self.isPresentingSearch = YES;
}
- (void)didPresentSearchController:(UISearchController *)searchController
{
self.isPresentingSearch = NO;
}
- (void)willDismissSearchController:(UISearchController *)searchController
{
[self.tableView reloadData];
}
@end
+63
View File
@@ -0,0 +1,63 @@
//
// FLEXNetworkRecorder.h
// Flipboard
//
// Created by Ryan Olson on 2/4/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
// Notifications posted when the record is updated
extern NSString *const kFLEXNetworkRecorderNewTransactionNotification;
extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
@class FLEXNetworkTransaction;
@interface FLEXNetworkRecorder : NSObject
/// In general, it only makes sense to have one recorder for the entire application.
+ (instancetype)defaultRecorder;
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
@property (nonatomic, assign) NSUInteger responseCacheByteLimit;
/// If NO, the recorder not cache will not cache response for content types with an "image", "video", or "audio" prefix.
@property (nonatomic, assign) BOOL shouldCacheMediaResponses;
// Accessing recorded network activity
/// Array of FLEXNetworkTransaction objects ordered by start time with the newest first.
- (NSArray *)networkTransactions;
/// The full response data IFF it hasn't been purged due to memory pressure.
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction;
/// Dumps all network transactions and cached response bodies.
- (void)clearRecordedActivity;
// Recording network activity
/// Call when app is about to send HTTP request.
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID request:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
/// Call when HTTP response is available.
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response;
/// Call when data chunk is received over the network.
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength;
/// Call when HTTP request has finished loading.
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody;
/// Call when HTTP request has failed to load.
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error;
/// Call to set the request mechanism anytime after recordRequestWillBeSent... has been called.
/// This string can be set to anything useful about the API used to make the request.
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID;
@end
+261
View File
@@ -0,0 +1,261 @@
//
// FLEXNetworkRecorder.m
// Flipboard
//
// Created by Ryan Olson on 2/4/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
#import "FLEXResources.h"
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
NSString *const kFLEXNetworkRecorderTransactionsClearedNotification = @"kFLEXNetworkRecorderTransactionsClearedNotification";
NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.responseCacheLimit";
@interface FLEXNetworkRecorder ()
@property (nonatomic, strong) NSCache *responseCache;
@property (nonatomic, strong) NSMutableArray *orderedTransactions;
@property (nonatomic, strong) NSMutableDictionary *networkTransactionsForRequestIdentifiers;
@property (nonatomic, strong) dispatch_queue_t queue;
@end
@implementation FLEXNetworkRecorder
- (instancetype)init
{
self = [super init];
if (self) {
self.responseCache = [[NSCache alloc] init];
NSUInteger responseCacheLimit = [[[NSUserDefaults standardUserDefaults] objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue];
if (responseCacheLimit) {
[self.responseCache setTotalCostLimit:responseCacheLimit];
} else {
// Default to 25 MB max. The cache will purge earlier if there is memory pressure.
[self.responseCache setTotalCostLimit:25 * 1024 * 1024];
}
self.orderedTransactions = [NSMutableArray array];
self.networkTransactionsForRequestIdentifiers = [NSMutableDictionary dictionary];
// Serial queue used because we use mutable objects that are not thread safe
self.queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
}
return self;
}
+ (instancetype)defaultRecorder
{
static FLEXNetworkRecorder *defaultRecorder = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultRecorder = [[[self class] alloc] init];
});
return defaultRecorder;
}
#pragma mark - Public Data Access
- (NSUInteger)responseCacheByteLimit
{
return [self.responseCache totalCostLimit];
}
- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit
{
[self.responseCache setTotalCostLimit:responseCacheByteLimit];
[[NSUserDefaults standardUserDefaults] setObject:@(responseCacheByteLimit) forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey];
}
- (NSArray *)networkTransactions
{
__block NSArray *transactions = nil;
dispatch_sync(self.queue, ^{
transactions = [self.orderedTransactions copy];
});
return transactions;
}
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction
{
return [self.responseCache objectForKey:transaction.requestID];
}
- (void)clearRecordedActivity
{
dispatch_async(self.queue, ^{
[self.responseCache removeAllObjects];
[self.orderedTransactions removeAllObjects];
[self.networkTransactionsForRequestIdentifiers removeAllObjects];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kFLEXNetworkRecorderTransactionsClearedNotification object:self];
});
});
}
#pragma mark - Network Events
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID request:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
{
NSDate *startDate = [NSDate date];
if (redirectResponse) {
[self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
}
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = [[FLEXNetworkTransaction alloc] init];
transaction.requestID = requestID;
transaction.request = request;
transaction.startTime = startDate;
[self.orderedTransactions insertObject:transaction atIndex:0];
[self.networkTransactionsForRequestIdentifiers setObject:transaction forKey:requestID];
transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
[self postNewTransactionNotificationWithTransaction:transaction];
});
}
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response
{
NSDate *responseDate = [NSDate date];
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
if (!transaction) {
return;
}
transaction.response = response;
transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength
{
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
if (!transaction) {
return;
}
transaction.receivedDataLength += dataLength;
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody
{
NSDate *finishedDate = [NSDate date];
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
if (!transaction) {
return;
}
transaction.transactionState = FLEXNetworkTransactionStateFinished;
transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
BOOL shouldCache = [responseBody length] > 0;
if (!self.shouldCacheMediaResponses) {
NSArray *ignoredMIMETypePrefixes = @[ @"audio", @"image", @"video" ];
for (NSString *ignoredPrefix in ignoredMIMETypePrefixes) {
shouldCache = shouldCache && ![transaction.response.MIMEType hasPrefix:ignoredPrefix];
}
}
if (shouldCache) {
[self.responseCache setObject:responseBody forKey:requestID cost:[responseBody length]];
}
NSString *mimeType = transaction.response.MIMEType;
if ([mimeType hasPrefix:@"image/"] && [responseBody length] > 0) {
// Thumbnail image previews on a separate background queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger maxPixelDimension = [[UIScreen mainScreen] scale] * 32.0;
transaction.responseThumbnail = [FLEXUtility thumbnailedImageWithMaxPixelDimension:maxPixelDimension fromImageData:responseBody];
[self postUpdateNotificationForTransaction:transaction];
});
} else if ([mimeType isEqual:@"application/json"]) {
transaction.responseThumbnail = [FLEXResources jsonIcon];
} else if ([mimeType isEqual:@"text/plain"]){
transaction.responseThumbnail = [FLEXResources textPlainIcon];
} else if ([mimeType isEqual:@"text/html"]) {
transaction.responseThumbnail = [FLEXResources htmlIcon];
} else if ([mimeType isEqual:@"application/x-plist"]) {
transaction.responseThumbnail = [FLEXResources plistIcon];
} else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
transaction.responseThumbnail = [FLEXResources binaryIcon];
} else if ([mimeType rangeOfString:@"javascript"].length > 0) {
transaction.responseThumbnail = [FLEXResources jsIcon];
} else if ([mimeType rangeOfString:@"xml"].length > 0) {
transaction.responseThumbnail = [FLEXResources xmlIcon];
} else if ([mimeType hasPrefix:@"audio"]) {
transaction.responseThumbnail = [FLEXResources audioIcon];
} else if ([mimeType hasPrefix:@"video"]) {
transaction.responseThumbnail = [FLEXResources videoIcon];
} else if ([mimeType hasPrefix:@"text"]) {
transaction.responseThumbnail = [FLEXResources textIcon];
}
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error
{
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
if (!transaction) {
return;
}
transaction.transactionState = FLEXNetworkTransactionStateFailed;
transaction.duration = -[transaction.startTime timeIntervalSinceNow];
transaction.error = error;
[self postUpdateNotificationForTransaction:transaction];
});
}
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID
{
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
if (!transaction) {
return;
}
transaction.requestMechanism = mechanism;
[self postUpdateNotificationForTransaction:transaction];
});
}
#pragma mark Notification Posting
- (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction
{
dispatch_async(dispatch_get_main_queue(), ^{
NSDictionary *userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
[[NSNotificationCenter defaultCenter] postNotificationName:kFLEXNetworkRecorderNewTransactionNotification object:self userInfo:userInfo];
});
}
- (void)postUpdateNotificationForTransaction:(FLEXNetworkTransaction *)transaction
{
dispatch_async(dispatch_get_main_queue(), ^{
NSDictionary *userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
[[NSNotificationCenter defaultCenter] postNotificationName:kFLEXNetworkRecorderTransactionUpdatedNotification object:self userInfo:userInfo];
});
}
@end
@@ -0,0 +1,13 @@
//
// FLEXNetworkSettingsTableViewController.h
// FLEXInjected
//
// Created by Ryan Olson on 2/20/15.
//
//
#import <UIKit/UIKit.h>
@interface FLEXNetworkSettingsTableViewController : UITableViewController
@end
@@ -0,0 +1,186 @@
//
// FLEXNetworkSettingsTableViewController.m
// FLEXInjected
//
// Created by Ryan Olson on 2/20/15.
//
//
#import "FLEXNetworkSettingsTableViewController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXUtility.h"
@interface FLEXNetworkSettingsTableViewController () <UIActionSheetDelegate>
@property (nonatomic, copy) NSArray *cells;
@property (nonatomic, strong) UITableViewCell *cacheLimitCell;
@end
@implementation FLEXNetworkSettingsTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *mutableCells = [NSMutableArray array];
UITableViewCell *networkDebuggingCell = [self switchCellWithTitle:@"Network Debugging" toggleAction:@selector(networkDebuggingToggled:) isOn:[FLEXNetworkObserver isEnabled]];
[mutableCells addObject:networkDebuggingCell];
UITableViewCell *cacheMediaResponsesCell = [self switchCellWithTitle:@"Cache Media Responses" toggleAction:@selector(cacheMediaResponsesToggled:) isOn:NO];
[mutableCells addObject:cacheMediaResponsesCell];
NSUInteger currentCacheLimit = [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
const NSUInteger fiftyMega = 50 * 1024 * 1024;
NSString *cacheLimitTitle = [self titleForCacheLimitCellWithValue:currentCacheLimit];
self.cacheLimitCell = [self sliderCellWithTitle:cacheLimitTitle changedAction:@selector(cacheLimitAdjusted:) minimum:0.0 maximum:fiftyMega initialValue:currentCacheLimit];
[mutableCells addObject:self.cacheLimitCell];
UITableViewCell *clearRecordedRequestsCell = [self buttonCellWithTitle:@"❌ Clear Recorded Requests" touchUpAction:@selector(clearRequestsTapped:) isDestructive:YES];
[mutableCells addObject:clearRecordedRequestsCell];
self.cells = mutableCells;
}
#pragma mark - Settings Actions
- (void)networkDebuggingToggled:(UISwitch *)sender
{
[FLEXNetworkObserver setEnabled:sender.isOn];
}
- (void)cacheMediaResponsesToggled:(UISwitch *)sender
{
[[FLEXNetworkRecorder defaultRecorder] setShouldCacheMediaResponses:sender.isOn];
}
- (void)cacheLimitAdjusted:(UISlider *)sender
{
[[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:sender.value];
self.cacheLimitCell.textLabel.text = [self titleForCacheLimitCellWithValue:sender.value];
}
- (void)clearRequestsTapped:(UIButton *)sender
{
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Clear Recorded Requests" otherButtonTitles:nil];
[actionSheet showInView:self.view];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.cells count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
return self.cells[indexPath.row];
}
#pragma mark - UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex != actionSheet.cancelButtonIndex) {
[[FLEXNetworkRecorder defaultRecorder] clearRecordedActivity];
}
}
#pragma mark - Helpers
- (UITableViewCell *)switchCellWithTitle:(NSString *)title toggleAction:(SEL)toggleAction isOn:(BOOL)isOn
{
UITableViewCell *cell = [[UITableViewCell alloc] init];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = title;
cell.textLabel.font = [[self class] cellTitleFont];
UISwitch *theSwitch = [[UISwitch alloc] init];
theSwitch.on = isOn;
[theSwitch addTarget:self action:toggleAction forControlEvents:UIControlEventValueChanged];
CGFloat switchOriginY = round((cell.contentView.frame.size.height - theSwitch.frame.size.height) / 2.0);
CGFloat switchOriginX = CGRectGetMaxX(cell.contentView.frame) - theSwitch.frame.size.width - self.tableView.separatorInset.left;
theSwitch.frame = CGRectMake(switchOriginX, switchOriginY, theSwitch.frame.size.width, theSwitch.frame.size.height);
theSwitch.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
[cell.contentView addSubview:theSwitch];
return cell;
}
- (UITableViewCell *)buttonCellWithTitle:(NSString *)title touchUpAction:(SEL)action isDestructive:(BOOL)isDestructive
{
UITableViewCell *buttonCell = [[UITableViewCell alloc] init];
buttonCell.selectionStyle = UITableViewCellSelectionStyleNone;
UIButton *actionButton = [UIButton buttonWithType:UIButtonTypeSystem];
[actionButton setTitle:title forState:UIControlStateNormal];
if (isDestructive) {
actionButton.tintColor = [UIColor redColor];
}
actionButton.titleLabel.font = [[self class] cellTitleFont];;
[actionButton addTarget:self action:@selector(clearRequestsTapped:) forControlEvents:UIControlEventTouchUpInside];
[buttonCell.contentView addSubview:actionButton];
actionButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
actionButton.frame = buttonCell.contentView.frame;
actionButton.contentEdgeInsets = UIEdgeInsetsMake(0.0, self.tableView.separatorInset.left, 0.0, self.tableView.separatorInset.left);
actionButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
return buttonCell;
}
- (NSString *)titleForCacheLimitCellWithValue:(long long)cacheLimit
{
NSInteger limitInMB = round(cacheLimit / (1024 * 1024));
return [NSString stringWithFormat:@"Cache Limit (%ld MB)", (long)limitInMB];
}
- (UITableViewCell *)sliderCellWithTitle:(NSString *)title changedAction:(SEL)changedAction minimum:(CGFloat)minimum maximum:(CGFloat)maximum initialValue:(CGFloat)initialValue
{
UITableViewCell *sliderCell = [[UITableViewCell alloc] init];
sliderCell.selectionStyle = UITableViewCellSelectionStyleNone;
sliderCell.textLabel.text = title;
sliderCell.textLabel.font = [[self class] cellTitleFont];
UISlider *slider = [[UISlider alloc] init];
slider.minimumValue = minimum;
slider.maximumValue = maximum;
slider.value = initialValue;
[slider addTarget:self action:changedAction forControlEvents:UIControlEventValueChanged];
[slider sizeToFit];
CGFloat sliderWidth = round(sliderCell.contentView.frame.size.width * 2.0 / 5.0);
CGFloat sliderOriginY = round((sliderCell.contentView.frame.size.height - slider.frame.size.height) / 2.0);
CGFloat sliderOriginX = CGRectGetMaxX(sliderCell.contentView.frame) - sliderWidth - self.tableView.separatorInset.left;
slider.frame = CGRectMake(sliderOriginX, sliderOriginY, sliderWidth, slider.frame.size.height);
slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
[sliderCell.contentView addSubview:slider];
return sliderCell;
}
+ (UIFont *)cellTitleFont
{
return [FLEXUtility defaultFontOfSize:14.0];
}
@end
+41
View File
@@ -0,0 +1,41 @@
//
// FLEXNetworkTransaction.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "UIKit/UIKit.h"
typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
FLEXNetworkTransactionStateUnstarted,
FLEXNetworkTransactionStateAwaitingResponse,
FLEXNetworkTransactionStateReceivingData,
FLEXNetworkTransactionStateFinished,
FLEXNetworkTransactionStateFailed
};
@interface FLEXNetworkTransaction : NSObject
@property (nonatomic, copy) NSString *requestID;
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSURLResponse *response;
@property (nonatomic, copy) NSString *requestMechanism;
@property (nonatomic, assign) FLEXNetworkTransactionState transactionState;
@property (nonatomic, strong) NSError *error;
@property (nonatomic, strong) NSDate *startTime;
@property (nonatomic, assign) NSTimeInterval latency;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, assign) int64_t receivedDataLength;
/// Only applicable for image downloads. A small thumbnail to preview the full response.
@property (nonatomic, strong) UIImage *responseThumbnail;
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
@end
+52
View File
@@ -0,0 +1,52 @@
//
// FLEXNetworkTransaction.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXNetworkTransaction.h"
@implementation FLEXNetworkTransaction
- (NSString *)description
{
NSString *description = [super description];
description = [description stringByAppendingFormat:@" id = %@;", self.requestID];
description = [description stringByAppendingFormat:@" url = %@;", self.request.URL];
description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
description = [description stringByAppendingFormat:@" receivedDataLength = %lld", self.receivedDataLength];
return description;
}
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state
{
NSString *readableString = nil;
switch (state) {
case FLEXNetworkTransactionStateUnstarted:
readableString = @"Unstarted";
break;
case FLEXNetworkTransactionStateAwaitingResponse:
readableString = @"Awaiting Response";
break;
case FLEXNetworkTransactionStateReceivingData:
readableString = @"Receiving Data";
break;
case FLEXNetworkTransactionStateFinished:
readableString = @"Finished";
break;
case FLEXNetworkTransactionStateFailed:
readableString = @"Failed";
break;
}
return readableString;
}
@end
@@ -0,0 +1,17 @@
//
// FLEXNetworkTransactionDetailTableViewController.h
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXNetworkTransaction;
@interface FLEXNetworkTransactionDetailTableViewController : UITableViewController
@property (nonatomic, strong) FLEXNetworkTransaction *transaction;
@end
@@ -0,0 +1,483 @@
//
// FLEXNetworkTransactionDetailTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXNetworkTransactionDetailTableViewController.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXUtility.h"
@interface FLEXNetworkDetailSection : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSArray *rows;
@end
@implementation FLEXNetworkDetailSection
@end
typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
@interface FLEXNetworkDetailRow : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *detailText;
@property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
@end
@implementation FLEXNetworkDetailRow
@end
@interface FLEXNetworkTransactionDetailTableViewController ()
@property (nonatomic, copy) NSArray *sections;
@end
@implementation FLEXNetworkTransactionDetailTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style
{
// Force grouped style
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionUpdatedNotification:) name:kFLEXNetworkRecorderTransactionUpdatedNotification object:nil];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonPressed:)];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineTableViewCellIdentifier];
}
- (void)setTransaction:(FLEXNetworkTransaction *)transaction
{
if (![_transaction isEqual:transaction]) {
_transaction = transaction;
self.title = [transaction.request.URL lastPathComponent];
[self rebuildTableSections];
}
}
- (void)setSections:(NSArray *)sections
{
if (![_sections isEqual:sections]) {
_sections = [sections copy];
[self.tableView reloadData];
}
}
- (void)rebuildTableSections
{
NSMutableArray *sections = [NSMutableArray array];
FLEXNetworkDetailSection *generalSection = [[self class] generalSectionForTransaction:self.transaction];
if ([generalSection.rows count] > 0) {
[sections addObject:generalSection];
}
FLEXNetworkDetailSection *requestHeadersSection = [[self class] requestHeadersSectionForTransaction:self.transaction];
if ([requestHeadersSection.rows count] > 0) {
[sections addObject:requestHeadersSection];
}
FLEXNetworkDetailSection *queryParametersSection = [[self class] queryParametersSectionForTransaction:self.transaction];
if ([queryParametersSection.rows count] > 0) {
[sections addObject:queryParametersSection];
}
FLEXNetworkDetailSection *postBodySection = [[self class] postBodySectionForTransaction:self.transaction];
if ([postBodySection.rows count] > 0) {
[sections addObject:postBodySection];
}
FLEXNetworkDetailSection *responseHeadersSection = [[self class] responseHeadersSectionForTransaction:self.transaction];
if ([responseHeadersSection.rows count] > 0) {
[sections addObject:responseHeadersSection];
}
self.sections = sections;
}
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
{
FLEXNetworkTransaction *transaction = [[notification userInfo] objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
if (transaction == self.transaction) {
[self rebuildTableSections];
}
}
- (void)copyButtonPressed:(id)sender
{
NSMutableString *requestDetailString = [NSMutableString string];
for (FLEXNetworkDetailSection *section in self.sections) {
if ([section.rows count] > 0) {
if ([section.title length] > 0) {
[requestDetailString appendString:section.title];
[requestDetailString appendString:@"\n\n"];
}
for (FLEXNetworkDetailRow *row in section.rows) {
NSString *rowDescription = [[[self class] attributedTextForRow:row] string];
if ([rowDescription length] > 0) {
[requestDetailString appendString:rowDescription];
[requestDetailString appendString:@"\n"];
}
}
[requestDetailString appendString:@"\n\n"];
}
}
[[UIPasteboard generalPasteboard] setString:requestDetailString];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [self.sections count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
FLEXNetworkDetailSection *sectionModel = self.sections[section];
return [sectionModel.rows count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
FLEXNetworkDetailSection *sectionModel = self.sections[section];
return sectionModel.title;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXMultilineTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXMultilineTableViewCellIdentifier forIndexPath:indexPath];
FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
cell.textLabel.attributedText = [[self class] attributedTextForRow:rowModel];
cell.accessoryType = rowModel.selectionFuture ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
cell.selectionStyle = rowModel.selectionFuture ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
UIViewController *viewControllerToPush = nil;
if (rowModel.selectionFuture) {
viewControllerToPush = rowModel.selectionFuture();
}
if (viewControllerToPush) {
[self.navigationController pushViewController:viewControllerToPush animated:YES];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
NSAttributedString *attributedText = [[self class] attributedTextForRow:row];
BOOL showsAccessory = row.selectionFuture != nil;
return [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.bounds.size.width style:UITableViewStyleGrouped showsAccessory:showsAccessory];
}
- (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath
{
FLEXNetworkDetailSection *sectionModel = self.sections[indexPath.section];
return sectionModel.rows[indexPath.row];
}
#pragma mark - Cell Copying
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(copy:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
[[UIPasteboard generalPasteboard] setString:row.detailText];
}
}
#pragma mark - View Configuration
+ (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row
{
NSDictionary *titleAttributes = @{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0],
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.5 alpha:1.0] };
NSDictionary *detailAttributes = @{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont],
NSForegroundColorAttributeName : [UIColor blackColor] };
NSString *title = [NSString stringWithFormat:@"%@: ", row.title];
NSString *detailText = row.detailText ?: @"";
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init];
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:titleAttributes]];
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:detailText attributes:detailAttributes]];
return attributedText;
}
#pragma mark - Table Data Generation
+ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
NSMutableArray *rows = [NSMutableArray array];
FLEXNetworkDetailRow *requestURLRow = [[FLEXNetworkDetailRow alloc] init];
requestURLRow.title = @"Request URL";
NSURL *url = transaction.request.URL;
requestURLRow.detailText = url.absoluteString;
requestURLRow.selectionFuture = ^{
UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
urlWebViewController.title = url.absoluteString;
return urlWebViewController;
};
[rows addObject:requestURLRow];
FLEXNetworkDetailRow *requestMethodRow = [[FLEXNetworkDetailRow alloc] init];
requestMethodRow.title = @"Request Method";
requestMethodRow.detailText = transaction.request.HTTPMethod;
[rows addObject:requestMethodRow];
if ([transaction.request.HTTPBody length] > 0) {
FLEXNetworkDetailRow *postBodySizeRow = [[FLEXNetworkDetailRow alloc] init];
postBodySizeRow.title = @"Request Body Size";
postBodySizeRow.detailText = [NSByteCountFormatter stringFromByteCount:[transaction.request.HTTPBody length] countStyle:NSByteCountFormatterCountStyleBinary];
[rows addObject:postBodySizeRow];
FLEXNetworkDetailRow *postBodyRow = [[FLEXNetworkDetailRow alloc] init];
postBodyRow.title = @"Request Body";
postBodyRow.detailText = @"tap to view";
postBodyRow.selectionFuture = ^{
NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
if (detailViewController) {
detailViewController.title = @"Request Body";
} else {
NSString *alertMessage = [NSString stringWithFormat:@"FLEX does not have a viewer for request body data with MIME type: %@", [transaction.request valueForHTTPHeaderField:@"Content-Type"]];
[[[UIAlertView alloc] initWithTitle:@"Can't View Body Data" message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
return detailViewController;
};
[rows addObject:postBodyRow];
}
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:transaction.response];
if ([statusCodeString length] > 0) {
FLEXNetworkDetailRow *statusCodeRow = [[FLEXNetworkDetailRow alloc] init];
statusCodeRow.title = @"Status Code";
statusCodeRow.detailText = statusCodeString;
[rows addObject:statusCodeRow];
}
if (transaction.error) {
FLEXNetworkDetailRow *errorRow = [[FLEXNetworkDetailRow alloc] init];
errorRow.title = @"Error";
errorRow.detailText = transaction.error.localizedDescription;
[rows addObject:errorRow];
}
FLEXNetworkDetailRow *responseBodyRow = [[FLEXNetworkDetailRow alloc] init];
responseBodyRow.title = @"Response Body";
NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
if ([responseData length] > 0) {
responseBodyRow.detailText = @"tap to view";
// Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
__weak NSData *weakResponseData = responseData;
responseBodyRow.selectionFuture = ^{
UIViewController *responseBodyDetailViewController = nil;
NSData *strongResponseData = weakResponseData;
if (strongResponseData) {
responseBodyDetailViewController = [self detailViewControllerForMIMEType:transaction.response.MIMEType data:strongResponseData];
if (!responseBodyDetailViewController) {
NSString *alertMessage = [NSString stringWithFormat:@"FLEX does not have a viewer for responses with MIME type: %@", transaction.response.MIMEType];
[[[UIAlertView alloc] initWithTitle:@"Can't View Response" message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
responseBodyDetailViewController.title = @"Response";
} else {
NSString *alertMessage = @"The response has been purged from the cache";
[[[UIAlertView alloc] initWithTitle:@"Can't View Response" message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
return responseBodyDetailViewController;
};
} else {
BOOL emptyResponse = transaction.receivedDataLength == 0;
responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
}
[rows addObject:responseBodyRow];
FLEXNetworkDetailRow *responseSizeRow = [[FLEXNetworkDetailRow alloc] init];
responseSizeRow.title = @"Response Size";
responseSizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
[rows addObject:responseSizeRow];
FLEXNetworkDetailRow *mimeTypeRow = [[FLEXNetworkDetailRow alloc] init];
mimeTypeRow.title = @"MIME Type";
mimeTypeRow.detailText = transaction.response.MIMEType;
[rows addObject:mimeTypeRow];
FLEXNetworkDetailRow *mechanismRow = [[FLEXNetworkDetailRow alloc] init];
mechanismRow.title = @"Mechanism";
mechanismRow.detailText = transaction.requestMechanism;
[rows addObject:mechanismRow];
NSDateFormatter *startTimeFormatter = [[NSDateFormatter alloc] init];
startTimeFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
FLEXNetworkDetailRow *localStartTimeRow = [[FLEXNetworkDetailRow alloc] init];
localStartTimeRow.title = [NSString stringWithFormat:@"Start Time (%@)", [[NSTimeZone localTimeZone] abbreviationForDate:transaction.startTime]];
localStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
[rows addObject:localStartTimeRow];
startTimeFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
FLEXNetworkDetailRow *utcStartTimeRow = [[FLEXNetworkDetailRow alloc] init];
utcStartTimeRow.title = @"Start Time (UTC)";
utcStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
[rows addObject:utcStartTimeRow];
FLEXNetworkDetailRow *unixStartTime = [[FLEXNetworkDetailRow alloc] init];
unixStartTime.title = @"Unix Start Time";
unixStartTime.detailText = [NSString stringWithFormat:@"%f", [transaction.startTime timeIntervalSince1970]];
[rows addObject:unixStartTime];
FLEXNetworkDetailRow *durationRow = [[FLEXNetworkDetailRow alloc] init];
durationRow.title = @"Total Duration";
durationRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.duration];
[rows addObject:durationRow];
FLEXNetworkDetailRow *latencyRow = [[FLEXNetworkDetailRow alloc] init];
latencyRow.title = @"Latency";
latencyRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.latency];
[rows addObject:latencyRow];
FLEXNetworkDetailSection *generalSection = [[FLEXNetworkDetailSection alloc] init];
generalSection.title = @"General";
generalSection.rows = rows;
return generalSection;
}
+ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
FLEXNetworkDetailSection *requestHeadersSection = [[FLEXNetworkDetailSection alloc] init];
requestHeadersSection.title = @"Request Headers";
requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
return requestHeadersSection;
}
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction
{
FLEXNetworkDetailSection *postBodySection = [[FLEXNetworkDetailSection alloc] init];
postBodySection.title = @"Request Body Parameters";
if ([transaction.request.HTTPBody length] > 0) {
NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
NSString *bodyString = [[NSString alloc] initWithData:[self postBodyDataForTransaction:transaction] encoding:NSUTF8StringEncoding];
postBodySection.rows = [self networkDetailRowsFromDictionary:[FLEXUtility dictionaryFromQuery:bodyString]];
}
}
return postBodySection;
}
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
NSDictionary *queryDictionary = [FLEXUtility dictionaryFromQuery:transaction.request.URL.query];
FLEXNetworkDetailSection *querySection = [[FLEXNetworkDetailSection alloc] init];
querySection.title = @"Query Parameters";
querySection.rows = [self networkDetailRowsFromDictionary:queryDictionary];
return querySection;
}
+ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
{
FLEXNetworkDetailSection *responseHeadersSection = [[FLEXNetworkDetailSection alloc] init];
responseHeadersSection.title = @"Response Headers";
if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)transaction.response;
responseHeadersSection.rows = [self networkDetailRowsFromDictionary:httpResponse.allHeaderFields];
}
return responseHeadersSection;
}
+ (NSArray *)networkDetailRowsFromDictionary:(NSDictionary *)dictionary
{
NSMutableArray *rows = [NSMutableArray arrayWithCapacity:[dictionary count]];
NSArray *sortedKeys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
for (NSString *key in sortedKeys) {
NSString *value = dictionary[key];
FLEXNetworkDetailRow *row = [[FLEXNetworkDetailRow alloc] init];
row.title = key;
row.detailText = [value description];
[rows addObject:row];
}
return [rows copy];
}
+ (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data
{
// FIXME (RKO): Don't rely on UTF8 string encoding
UIViewController *detailViewController = nil;
if ([FLEXUtility isValidJSONData:data]) {
NSString *prettyJSON = [FLEXUtility prettyJSONStringFromData:data];
if ([prettyJSON length] > 0) {
detailViewController = [[FLEXWebViewController alloc] initWithText:prettyJSON];
}
} else if ([mimeType hasPrefix:@"image/"]) {
UIImage *image = [UIImage imageWithData:data];
detailViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else if ([mimeType isEqual:@"application/x-plist"]) {
id propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
detailViewController = [[FLEXWebViewController alloc] initWithText:[propertyList description]];
}
// Fall back to trying to show the response as text
if (!detailViewController) {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if ([text length] > 0) {
detailViewController = [[FLEXWebViewController alloc] initWithText:text];
}
}
return detailViewController;
}
+ (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction
{
NSData *bodyData = transaction.request.HTTPBody;
if ([bodyData length] > 0) {
NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
if ([contentEncoding rangeOfString:@"deflate" options:NSCaseInsensitiveSearch].length > 0 || [contentEncoding rangeOfString:@"gzip" options:NSCaseInsensitiveSearch].length > 0) {
bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
}
}
return bodyData;
}
@end
@@ -0,0 +1,21 @@
//
// FLEXNetworkTransactionTableViewCell.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
extern NSString *const kFLEXNetworkTransactionCellIdentifier;
@class FLEXNetworkTransaction;
@interface FLEXNetworkTransactionTableViewCell : UITableViewCell
@property (nonatomic, strong) FLEXNetworkTransaction *transaction;
+ (CGFloat)preferredCellHeight;
@end
@@ -0,0 +1,180 @@
//
// FLEXNetworkTransactionTableViewCell.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXNetworkTransactionTableViewCell.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
#import "FLEXResources.h"
NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
@interface FLEXNetworkTransactionTableViewCell ()
@property (nonatomic, strong) UIImageView *thumbnailImageView;
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *pathLabel;
@property (nonatomic, strong) UILabel *transactionDetailsLabel;
@end
@implementation FLEXNetworkTransactionTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
self.nameLabel = [[UILabel alloc] init];
self.nameLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
[self.contentView addSubview:self.nameLabel];
self.pathLabel = [[UILabel alloc] init];
self.pathLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
self.pathLabel.textColor = [UIColor colorWithWhite:0.4 alpha:1.0];
[self.contentView addSubview:self.pathLabel];
self.thumbnailImageView = [[UIImageView alloc] init];
self.thumbnailImageView.layer.borderColor = [[UIColor blackColor] CGColor];
self.thumbnailImageView.layer.borderWidth = 1.0;
self.thumbnailImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:self.thumbnailImageView];
self.transactionDetailsLabel = [[UILabel alloc] init];
self.transactionDetailsLabel.font = [FLEXUtility defaultFontOfSize:10.0];
self.transactionDetailsLabel.textColor = [UIColor colorWithWhite:0.65 alpha:1.0];
[self.contentView addSubview:self.transactionDetailsLabel];
}
return self;
}
- (void)setTransaction:(FLEXNetworkTransaction *)transaction
{
if (_transaction != transaction) {
_transaction = transaction;
[self setNeedsLayout];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
const CGFloat kVerticalPadding = 8.0;
const CGFloat kLeftPadding = 10.0;
const CGFloat kImageDimension = 32.0;
CGFloat thumbnailOriginY = round((self.contentView.bounds.size.height - kImageDimension) / 2.0);
self.thumbnailImageView.frame = CGRectMake(kLeftPadding, thumbnailOriginY, kImageDimension, kImageDimension);
self.thumbnailImageView.image = self.transaction.responseThumbnail;
CGFloat textOriginX = CGRectGetMaxX(self.thumbnailImageView.frame) + kLeftPadding;
CGFloat availableTextWidth = self.contentView.bounds.size.width - textOriginX;
self.nameLabel.text = [self nameLabelText];
CGSize nameLabelPreferredSize = [self.nameLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
self.nameLabel.frame = CGRectMake(textOriginX, kVerticalPadding, availableTextWidth, nameLabelPreferredSize.height);
self.nameLabel.textColor = self.transaction.error ? [UIColor redColor] : [UIColor blackColor];
self.pathLabel.text = [self pathLabelText];
CGSize pathLabelPreferredSize = [self.pathLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
CGFloat pathLabelOriginY = ceil((self.contentView.bounds.size.height - pathLabelPreferredSize.height) / 2.0);
self.pathLabel.frame = CGRectMake(textOriginX, pathLabelOriginY, availableTextWidth, pathLabelPreferredSize.height);
self.transactionDetailsLabel.text = [self transactionDetailsLabelText];
CGSize transactionLabelPreferredSize = [self.transactionDetailsLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
CGFloat transactionDetailsOriginX = textOriginX;
CGFloat transactionDetailsLabelOriginY = CGRectGetMaxY(self.contentView.bounds) - kVerticalPadding - transactionLabelPreferredSize.height;
CGFloat transactionDetailsLabelWidth = self.contentView.bounds.size.width - transactionDetailsOriginX;
self.transactionDetailsLabel.frame = CGRectMake(transactionDetailsOriginX, transactionDetailsLabelOriginY, transactionDetailsLabelWidth, transactionLabelPreferredSize.height);
}
- (NSString *)nameLabelText
{
NSURL *url = self.transaction.request.URL;
NSString *name = [url lastPathComponent];
if ([name length] == 0) {
name = @"/";
}
NSString *query = [url query];
if (query) {
name = [name stringByAppendingFormat:@"?%@", query];
}
return name;
}
- (NSString *)pathLabelText
{
NSURL *url = self.transaction.request.URL;
NSMutableArray *mutablePathComponents = [[url pathComponents] mutableCopy];
if ([mutablePathComponents count] > 0) {
[mutablePathComponents removeLastObject];
}
NSString *path = [url host];
for (NSString *pathComponent in mutablePathComponents) {
path = [path stringByAppendingPathComponent:pathComponent];
}
return path;
}
- (NSString *)transactionDetailsLabelText
{
NSMutableArray *detailComponents = [NSMutableArray array];
NSString *timestamp = [[self class] timestampStringFromRequestDate:self.transaction.startTime];
if ([timestamp length] > 0) {
[detailComponents addObject:timestamp];
}
// Omit method for GET (assumed as default)
NSString *httpMethod = self.transaction.request.HTTPMethod;
if ([httpMethod length] > 0) {
[detailComponents addObject:httpMethod];
}
if (self.transaction.transactionState == FLEXNetworkTransactionStateFinished || self.transaction.transactionState == FLEXNetworkTransactionStateFailed) {
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.transaction.response];
if ([statusCodeString length] > 0) {
[detailComponents addObject:statusCodeString];
}
if (self.transaction.receivedDataLength > 0) {
NSString *responseSize = [NSByteCountFormatter stringFromByteCount:self.transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
[detailComponents addObject:responseSize];
}
NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.transaction.duration];
NSString *latency = [FLEXUtility stringFromRequestDuration:self.transaction.latency];
NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
[detailComponents addObject:duration];
} else {
// Unstarted, Awaiting Response, Receiving Data, etc.
NSString *state = [FLEXNetworkTransaction readableStringFromTransactionState:self.transaction.transactionState];
[detailComponents addObject:state];
}
return [detailComponents componentsJoinedByString:@""];
}
+ (NSString *)timestampStringFromRequestDate:(NSDate *)date
{
static NSDateFormatter *dateFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"HH:mm:ss";
});
return [dateFormatter stringFromDate:date];
}
+ (CGFloat)preferredCellHeight
{
return 65.0;
}
@end
@@ -0,0 +1,29 @@
//
// FLEXNetworkObserver.h
// Derived from:
//
// PDAFNetworkDomainController.h
// PonyDebugger
//
// Created by Mike Lewis on 2/27/12.
//
// Licensed to Square, Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Square, Inc. licenses this file to you.
//
#import <Foundation/Foundation.h>
extern NSString *const kFLEXNetworkObserverEnabledStateChangedNotification;
/// This class swizzles NSURLConnection and NSURLSession delegate methods to observe events in the URL loading system.
/// High level network events are sent to the default FLEXNetworkRecorder instance which maintains the request history and caches response bodies.
@interface FLEXNetworkObserver : NSObject
/// Swizzling occurs when the observer is enabled for the first time.
/// This reduces the impact of FLEX if network debugging is not desired.
/// NOTE: this setting persists between launches of the app.
+ (void)setEnabled:(BOOL)enabled;
+ (BOOL)isEnabled;
@end

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