Compare commits

...

265 Commits

Author SHA1 Message Date
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
Ryan Olson a4573600e7 Bump version in .podspec 2014-07-31 00:06:48 -07:00
Ryan Olson d96c911ade Update change list in README 2014-07-31 00:05:41 -07:00
Ryan Olson dc5b5b1371 Initialize userGlobalEntries in -[FLEXManager init] 2014-07-30 17:41:42 -07:00
Ryan Olson d4b8f5eba5 Merge pull request #3 from JaviSoto/feature/custom-global-entries
Added an API to register custom entries to the Globals table view controller
2014-07-30 17:29:26 -07:00
Javier Soto 873552aa6c Style fixes 2014-07-30 17:23:48 -07:00
Ryan Olson b04a5fdf4e Fix ivar value display in FLEXObjectExplorerViewController.
Everything has been showing nil since 9006b194cd
2014-07-29 18:34:58 -07:00
Ryan Olson 5077883681 Add “Changes” section to README 2014-07-28 23:27:57 -07:00
Ryan Olson 2862c3ee97 Merge pull request #7 from DaidoujiChen/develop
Add font picker input view
2014-07-28 23:13:36 -07:00
DaidoujiChen 925cc4bff6 avoid use magic number to alloc toolbar 2014-07-29 13:55:02 +08:00
Ryan Olson 5be95ddd95 Fix status bar styling for apps that use global status bar management. 2014-07-28 22:54:24 -07:00
Ryan Olson 190e5336f7 Forward shouldAutorotate in addition to supportedInterfaceOrientations.
We want the app to fully control rotation behavior regardless of whether FLEX is presented or not.
2014-07-28 22:29:21 -07:00
Ryan Olson 4e0313ad28 Merge pull request #1 from JaviSoto/master
Fixed some warnings
2014-07-28 21:47:28 -07:00
Ryan Olson d01f4fb0ee Add CLA info to README 2014-07-28 18:00:37 -07:00
Javier Soto e027b2944c No +initialize or static global variables, created a class method that gives you the default global entries on-demand. 2014-07-28 13:13:23 -07:00
DaidoujiChen 4206a3a5f9 remove line break in method 2014-07-28 15:59:15 +08:00
DaidoujiChen 61a483928d modify code style 2014-07-28 15:35:41 +08:00
DaidoujiChen fdce03170e FLEXArgumentInputFontsPickerView never through FLEXArgumentInputViewFactory 2014-07-28 12:18:13 +08:00
DaidoujiChen 5de0b9ec62 fix issues on ios7 2014-07-28 12:04:36 +08:00
DaidoujiChen 4e9b931456 add done button in FLEXArgumentInputTextView 2014-07-27 19:12:29 +08:00
DaidoujiChen 842bc94525 add FLEXArgumentInputFontsPickerView, easily select fonts 2014-07-27 18:44:12 +08:00
Javier Soto 8760539c68 Global variables are bad, but mutable global variables are worse! And we don't need to append to this object anymore, so just making s_defaultGlobalEntries an NSArray. 2014-07-26 16:46:00 -07:00
Javier Soto baf02c7e63 Moved the API to add user global entries to FLEXManager 2014-07-26 16:43:23 -07:00
Javier Soto 59d7c2e2d8 Moved FLEXGlobalsTableViewControllerEntry into its own file 2014-07-26 16:42:55 -07:00
Javier Soto ca2594fd15 Changed the order of the items in the FLEXGlobalsRow enum to keep the original ordering of elements. 2014-07-26 16:26:53 -07:00
Javier Soto e6686d0708 Double ;; 2014-07-26 16:20:07 -07:00
Javier Soto 41d6c35cc2 Naming future what's a future for clarity. 2014-07-26 16:17:59 -07:00
Javier Soto b1bf34cdcd Not breaking method declarations in multiple lines for consistency 2014-07-26 16:13:51 -07:00
Javier Soto 0982c5108c Made the static application window reference __weak so that we don't keep it around longer than it exists. 2014-07-26 16:12:42 -07:00
Javier Soto c34f064c3e Merge branch 'master' of github.com:JaviSoto/FLEX into feature/custom-global-entries 2014-07-25 13:31:30 -07:00
Javier Soto 9547151c38 Added an API to register custom entries in the Globals table view controller.
This way 3rd party applications can extend the functionality of FLEX by adding quick access to important objects in their applications.
I modified FLEXGlobalsTableViewController to keep track of the items to display in the table with a list of FLEXGlobalsTableViewControllerEntry objects which have 2 blocks: one that gives you the name of the entry to display, and one that gives you the view controller to push when that entry is selected. I used a block to make the creation of both lazy because they can change (for example in case the application window changes).
I exposed this partially in the API: users can pass a reference to an object. This way they can set this app lazily in a +load method in some class, and not wait until the object they want to be able to inspect with FLEX is actually created, which simplifies the use (but this may be my brain being contaminated by ReactiveCocoa, so I'm definitely open to criticism!)

Open questions:
Should this API be in FLEXGlobalsTableViewController, which means that users of the API must #import that header, or should it be in FLEXManager?
2014-07-25 12:19:54 -07:00
Javier Soto 3f51ece152 Fix indentation 2014-07-25 10:54:54 -07:00
Javier Soto 4ffacae080 Fixed warning: multiple accesses to weak variable.
Not a huge deal in this case, but it's always a good idea to get a strong reference to the object we only have a weak reference to so that it can't go away half-way through the execution of a block (which in some cases can cause bugs that are super hard to track down)
2014-07-25 10:41:11 -07:00
Javier Soto d9da4eb35f Fixed warnings: comparisons between unsigned and signed integers. 2014-07-25 10:40:16 -07:00
146 changed files with 7687 additions and 831 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
@@ -9,6 +9,7 @@
#import "FLEXArgumentInputFontView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXArgumentInputFontsPickerView.h"
@interface FLEXArgumentInputFontView ()
@@ -23,7 +24,7 @@
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.fontNameInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:FLEXEncodeClass(NSString)];
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
self.fontNameInput.backgroundColor = self.backgroundColor;
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.fontNameInput.title = @"Font Name:";
@@ -0,0 +1,12 @@
//
// FLEXArgumentInputFontsPickerView.h
// UICatalog
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
//
#import "FLEXArgumentInputTextView.h"
@interface FLEXArgumentInputFontsPickerView : FLEXArgumentInputTextView <UIPickerViewDataSource, UIPickerViewDelegate>
@end
@@ -0,0 +1,105 @@
//
// FLEXArgumentInputFontsPickerView.m
// UICatalog
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
//
#import "FLEXArgumentInputFontsPickerView.h"
#import "FLEXRuntimeUtility.h"
@interface FLEXArgumentInputFontsPickerView ()
@property (nonatomic, strong) NSMutableArray *availableFonts;
@end
@implementation FLEXArgumentInputFontsPickerView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
[self createAvailableFonts];
self.inputTextView.inputView = [self createFontsPicker];
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = inputValue;
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
[self.availableFonts insertObject:inputValue atIndex:0];
}
[(UIPickerView*)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
}
- (id)inputValue
{
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
}
#pragma mark - private
- (UIPickerView*)createFontsPicker
{
UIPickerView *fontsPicker = [UIPickerView new];
fontsPicker.dataSource = self;
fontsPicker.delegate = self;
fontsPicker.showsSelectionIndicator = YES;
return fontsPicker;
}
- (void)createAvailableFonts
{
NSMutableArray *unsortedFontsArray = [NSMutableArray array];
for (NSString *eachFontFamily in [UIFont familyNames]) {
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
[unsortedFontsArray addObject:eachFontName];
}
}
self.availableFonts = [NSMutableArray arrayWithArray:[unsortedFontsArray sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]];
}
#pragma mark - UIPickerViewDataSource
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [self.availableFonts count];
}
#pragma mark - UIPickerViewDelegate
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
UILabel *fontLabel;
if (!view) {
fontLabel = [UILabel new];
fontLabel.backgroundColor = [UIColor clearColor];
fontLabel.textAlignment = NSTextAlignmentCenter;
} else {
fontLabel = (UILabel*)view;
}
UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0];
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary];
fontLabel.attributedText = attributesString;
[fontLabel sizeToFit];
return fontLabel;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
self.inputTextView.text = self.availableFonts[row];
}
@end
@@ -30,11 +30,29 @@
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
[self addSubview:self.inputTextView];
}
return self;
}
#pragma mark - private
- (UIToolbar*)createToolBar
{
UIToolbar *toolBar = [UIToolbar new];
[toolBar sizeToFit];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(textViewDone)];
toolBar.items = @[spaceItem, doneItem];
return toolBar;
}
- (void)textViewDone
{
[self.inputTextView resignFirstResponder];
}
#pragma mark - Text View Changes
@@ -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];
}
-20
View File
@@ -1,20 +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;
@end
-79
View File
@@ -1,79 +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"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
@property (nonatomic, strong) FLEXWindow *explorerWindow;
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
@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;
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];
}
@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,7 @@
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
- (BOOL)wantsWindowToBecomeKey;
@end
@@ -58,6 +58,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
/// If we're just showing the toolbar, we want the main app's window to remain key so that we don't interfere with input, status bar, etc.
@property (nonatomic, strong) UIWindow *previousKeyWindow;
/// Similar to the previousKeyWindow property above, we need to track status bar styling if
/// the app doesn't use view controller based status bar management. When we present a modal,
/// we want to change the status bar style to UIStausBarStyleDefault. Before changing, we stash
/// the current style. On dismissal, we return the staus bar to the style that the app was using previously.
@property (nonatomic, assign) UIStatusBarStyle previousStatusBarStyle;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic, strong) NSMutableSet *observedViews;
@@ -114,63 +120,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];
}
@@ -184,6 +154,16 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return supportedOrientations;
}
- (BOOL)shouldAutorotate
{
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
}
return shouldAutorotate;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
@@ -419,20 +399,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;
}
@@ -455,7 +438,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
globalsViewController.delegate = self;
globalsViewController.applicationWindow = [[UIApplication sharedApplication] keyWindow];
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
}
@@ -564,7 +547,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];
}
}
@@ -719,8 +706,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;
}
@@ -811,21 +798,36 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
// for view controller based management because the global methods no-op if that is turned on.
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
}
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
{
[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.
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
[self dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
return self.previousKeyWindow != nil;
}
@end
@@ -0,0 +1,16 @@
//
// FLEXManager+Private.h
// PebbleApp
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 Pebble Technology. All rights reserved.
//
#import "FLEXManager.h"
@interface FLEXManager ()
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
@property (nonatomic, readonly, strong) NSArray *userGlobalEntries;
@end
+50
View File
@@ -0,0 +1,50 @@
//
// 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;
/// 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 50 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 - 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
+165
View File
@@ -0,0 +1,165 @@
//
// 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"
@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;
}
- (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 - 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];
}
@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,180 +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(), ^{
weakSelf.recursiveSize = @(totalSize);
[weakSelf.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
@@ -1,231 +0,0 @@
//
// FLEXGlobalsTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXGlobalsTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowFileBrowser
};
@interface FLEXGlobalsTableViewController ()
@property (nonatomic, strong) NSArray *rows;
@end
@implementation FLEXGlobalsTableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
self.title = @"🌎 Global State";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.rows = @[@(FLEXGlobalsRowLiveObjects),
@(FLEXGlobalsRowFileBrowser),
@(FLEXGlobalsRowSystemLibraries),
@(FLEXGlobalsRowAppClasses),
@(FLEXGlobalsRowAppDelegate),
@(FLEXGlobalsRowRootViewController),
@(FLEXGlobalsRowUserDefaults),
@(FLEXGlobalsRowApplication),
@(FLEXGlobalsRowKeyWindow),
@(FLEXGlobalsRowMainScreen),
@(FLEXGlobalsRowCurrentDevice)];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
}
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsRow)rowTypeAtIndexPath:(NSIndexPath *)indexPath
{
return [[self.rows objectAtIndex:indexPath.row] unsignedIntegerValue];
}
- (NSString *)titleForRowType:(FLEXGlobalsRow)rowType
{
NSString *title = nil;
switch (rowType) {
case FLEXGlobalsRowAppClasses:
title = [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
break;
case FLEXGlobalsRowSystemLibraries:
title = @"📚 System Libraries";
break;
case FLEXGlobalsRowLiveObjects:
title = @"💩 Heap Objects";
break;
case FLEXGlobalsRowAppDelegate:
title = [NSString stringWithFormat:@"👉 %@", [[[UIApplication sharedApplication] delegate] class]];
break;
case FLEXGlobalsRowRootViewController:
title = [NSString stringWithFormat:@"🌴 %@", [[self.applicationWindow rootViewController] class]];
break;
case FLEXGlobalsRowUserDefaults:
title = @"🚶 +[NSUserDefaults standardUserDefaults]";
break;
case FLEXGlobalsRowApplication:
title = @"💾 +[UIApplication sharedApplication]";
break;
case FLEXGlobalsRowKeyWindow:
title = @"🔑 -[UIApplication keyWindow]";
break;
case FLEXGlobalsRowMainScreen:
title = @"💻 +[UIScreen mainScreen]";
break;
case FLEXGlobalsRowCurrentDevice:
title = @"📱 +[UIDevice currentDevice]";
break;
case FLEXGlobalsRowFileBrowser:
title = @"📁 File Browser";
break;
}
return title;
}
- (UIViewController *)drillDownViewControllerForRowType:(FLEXGlobalsRow)rowType
{
UIViewController *viewController = nil;
switch (rowType) {
case FLEXGlobalsRowAppClasses: {
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
classesViewController.binaryImageName = [FLEXUtility applicationImageName];
viewController = classesViewController;
} break;
case FLEXGlobalsRowSystemLibraries: {
FLEXLibrariesTableViewController *librariesViewController = [[FLEXLibrariesTableViewController alloc] init];
librariesViewController.title = [self titleForRowType:FLEXGlobalsRowSystemLibraries];
viewController = librariesViewController;
} break;
case FLEXGlobalsRowLiveObjects: {
viewController = [[FLEXLiveObjectsTableViewController alloc] init];
} break;
case FLEXGlobalsRowAppDelegate: {
id <UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:appDelegate];
} break;
case FLEXGlobalsRowRootViewController: {
UIViewController *rootViewController = [self.applicationWindow rootViewController];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:rootViewController];
} break;
case FLEXGlobalsRowUserDefaults: {
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:standardUserDefaults];
} break;
case FLEXGlobalsRowApplication: {
UIApplication *sharedApplication = [UIApplication sharedApplication];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:sharedApplication];
} break;
case FLEXGlobalsRowKeyWindow: {
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.applicationWindow];
} break;
case FLEXGlobalsRowMainScreen: {
UIScreen *mainScreen = [UIScreen mainScreen];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:mainScreen];
} break;
case FLEXGlobalsRowCurrentDevice: {
UIDevice *currentDevice = [UIDevice currentDevice];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentDevice];
} break;
case FLEXGlobalsRowFileBrowser: {
viewController = [[FLEXFileBrowserTableViewController alloc] init];
}
}
return viewController;
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.rows count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
}
FLEXGlobalsRow rowType = [self rowTypeAtIndexPath:indexPath];
cell.textLabel.text = [self titleForRowType:rowType];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsRow rowType = [self rowTypeAtIndexPath:indexPath];
UIViewController *drillDownViewController = [self drillDownViewControllerForRowType:rowType];
[self.navigationController pushViewController:drillDownViewController animated:YES];
}
@end
@@ -54,7 +54,7 @@
const char **classNames = objc_copyClassNamesForImage([self.binaryImageName UTF8String], &classNamesCount);
if (classNames) {
NSMutableArray *classNameStrings = [NSMutableArray array];
for (int i = 0; i < classNamesCount; i++) {
for (unsigned int i = 0; i < classNamesCount; i++) {
const char *className = classNames[i];
NSString *classNameString = [NSString stringWithUTF8String:className];
[classNameStrings addObject:classNameString];
@@ -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.
@@ -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,7 +8,9 @@
#import <UIKit/UIKit.h>
@interface FLEXFileBrowserTableViewController : UITableViewController
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserTableViewController : UITableViewController <UISearchDisplayDelegate, FLEXFileBrowserSearchOperationDelegate>
- (id)initWithPath:(NSString *)path;
@@ -0,0 +1,391 @@
//
// 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"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserFileOperationControllerDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray *childPaths;
@property (nonatomic, copy) NSString *searchString;
@property (nonatomic, strong) NSArray *searchPaths;
@property (nonatomic, strong) NSNumber *recursiveSize;
@property (nonatomic, strong) NSNumber *searchPathsSize;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@property (nonatomic, strong) UISearchDisplayController *searchController;
#pragma clang diagnostic pop
@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];
//add search controller
UISearchBar *searchBar = [UISearchBar new];
[searchBar sizeToFit];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
#pragma clang diagnostic pop
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
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.searchController.searchResultsTableView reloadData];
}
#pragma mark - UISearchDisplayDelegate
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
self.searchString = searchString;
[self reloadSearchPaths];
return YES;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willHideSearchResultsTableView:(UITableView *)tableView
{
//confirm to clear all operations
[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
{
if (tableView == self.tableView) {
return [self.childPaths count];
} else {
return [self.searchPaths count];
}
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSNumber *currentSize = nil;
NSArray *currentPaths = nil;
if (tableView == self.tableView) {
currentSize = self.recursiveSize;
currentPaths = self.childPaths;
} else {
currentSize = self.searchPathsSize;
currentPaths = self.searchPaths;
}
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 = nil;
if (tableView == self.tableView) {
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
}
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 *subpath = nil;
NSString *fullPath = nil;
if (tableView == self.tableView) {
subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
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 {
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
{
NSString *fullPath = nil;
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
if (indexPath) {
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
indexPath = [self.searchController.searchResultsTableView indexPathForCell:sender];
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
}
self.fileOperationController = [[FLEXFileBrowserFileRenameOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
NSString *fullPath = nil;
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
if (indexPath) {
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
indexPath = [self.searchController.searchResultsTableView indexPathForCell:sender];
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
}
self.fileOperationController = [[FLEXFileBrowserFileDeleteOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self reloadSearchPaths];
[self.searchController.searchResultsTableView reloadData];
} else {
[self reloadChildPaths];
[self.tableView reloadData];
}
}
- (void)reloadChildPaths
{
self.childPaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:NULL];
}
- (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.searchString];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
}
@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
@@ -16,7 +16,7 @@
/// We pretend that one of the app's windows is still the key window, even though the explorer window may have become key.
/// We want to display debug state about the application, not about this tool.
@property (nonatomic, strong) UIWindow *applicationWindow;
+ (void)setApplicationWindow:(UIWindow *)applicationWindow;
@end
@@ -0,0 +1,295 @@
//
// FLEXGlobalsTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXGlobalsTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "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,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowCount
};
@interface FLEXGlobalsTableViewController ()
/// [FLEXGlobalsTableViewControllerEntry *]
@property (nonatomic, readonly, copy) NSArray *entries;
@end
@implementation FLEXGlobalsTableViewController
/// [FLEXGlobalsTableViewControllerEntry *]
+ (NSArray *)defaultGlobalEntries
{
NSMutableArray *defaultGlobalEntries = [NSMutableArray array];
for (FLEXGlobalsRow defaultRowIndex = 0; defaultRowIndex < FLEXGlobalsRowCount; defaultRowIndex++) {
FLEXGlobalsTableViewControllerEntryNameFuture titleFuture = nil;
FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture = nil;
switch (defaultRowIndex) {
case FLEXGlobalsRowAppClasses:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
};
viewControllerFuture = ^UIViewController *{
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
classesViewController.binaryImageName = [FLEXUtility applicationImageName];
return classesViewController;
};
break;
case FLEXGlobalsRowSystemLibraries: {
NSString *titleString = @"📚 System Libraries";
titleFuture = ^NSString *{
return titleString;
};
viewControllerFuture = ^UIViewController *{
FLEXLibrariesTableViewController *librariesViewController = [[FLEXLibrariesTableViewController alloc] init];
librariesViewController.title = titleString;
return librariesViewController;
};
break;
}
case FLEXGlobalsRowLiveObjects:
titleFuture = ^NSString *{
return @"💩 Heap Objects";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXLiveObjectsTableViewController alloc] init];
};
break;
case FLEXGlobalsRowAppDelegate:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"👉 %@", [[[UIApplication sharedApplication] delegate] class]];
};
viewControllerFuture = ^UIViewController *{
id <UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:appDelegate];
};
break;
case FLEXGlobalsRowRootViewController:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"🌴 %@", [[s_applicationWindow rootViewController] class]];
};
viewControllerFuture = ^UIViewController *{
UIViewController *rootViewController = [s_applicationWindow rootViewController];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:rootViewController];
};
break;
case FLEXGlobalsRowUserDefaults:
titleFuture = ^NSString *{
return @"🚶 +[NSUserDefaults standardUserDefaults]";
};
viewControllerFuture = ^UIViewController *{
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:standardUserDefaults];
};
break;
case FLEXGlobalsRowApplication:
titleFuture = ^NSString *{
return @"💾 +[UIApplication sharedApplication]";
};
viewControllerFuture = ^UIViewController *{
UIApplication *sharedApplication = [UIApplication sharedApplication];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:sharedApplication];
};
break;
case FLEXGlobalsRowKeyWindow:
titleFuture = ^NSString *{
return @"🔑 -[UIApplication keyWindow]";
};
viewControllerFuture = ^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
};
break;
case FLEXGlobalsRowMainScreen:
titleFuture = ^NSString *{
return @"💻 +[UIScreen mainScreen]";
};
viewControllerFuture = ^UIViewController *{
UIScreen *mainScreen = [UIScreen mainScreen];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:mainScreen];
};
break;
case FLEXGlobalsRowCurrentDevice:
titleFuture = ^NSString *{
return @"📱 +[UIDevice currentDevice]";
};
viewControllerFuture = ^UIViewController *{
UIDevice *currentDevice = [UIDevice currentDevice];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:currentDevice];
};
break;
case FLEXGlobalsRowFileBrowser:
titleFuture = ^NSString *{
return @"📁 File Browser";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXFileBrowserTableViewController alloc] init];
};
break;
case FLEXGlobalsRowSystemLog:
titleFuture = ^{
return @"⚠️ System Log";
};
viewControllerFuture = ^{
return [[FLEXSystemLogTableViewController alloc] init];
};
break;
case FLEXGlobalsRowNetworkHistory:
titleFuture = ^{
return @"📡 Network History";
};
viewControllerFuture = ^{
return [[FLEXNetworkHistoryTableViewController alloc] init];
};
break;
case FLEXGlobalsRowCount:
break;
}
NSParameterAssert(titleFuture);
NSParameterAssert(viewControllerFuture);
[defaultGlobalEntries addObject:[FLEXGlobalsTableViewControllerEntry entryWithNameFuture:titleFuture viewControllerFuture:viewControllerFuture]];
}
return defaultGlobalEntries;
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
self.title = @"💪 FLEX";
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
}
return self;
}
#pragma mark - Public
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
{
s_applicationWindow = applicationWindow;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
}
#pragma mark -
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsTableViewControllerEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
{
return self.entries[indexPath.row];
}
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.entryNameFuture();
}
- (UIViewController *)viewControllerToPushForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.viewControllerFuture();
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.entries count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIViewController *viewControllerToPush = [self viewControllerToPushForRowAtIndexPath:indexPath];
[self.navigationController pushViewController:viewControllerToPush animated:YES];
}
@end
@@ -102,7 +102,7 @@
id instance = [self.instances objectAtIndex:indexPath.row];
NSString *title = nil;
if ([self.fieldNames count] > indexPath.row) {
if ((NSInteger)[self.fieldNames count] > indexPath.row) {
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), [self.fieldNames objectAtIndex:indexPath.row]];
} else {
title = [NSString stringWithFormat:@"%@ %p", NSStringFromClass(object_getClass(instance)), instance];
@@ -52,7 +52,7 @@
if (imageNames) {
NSMutableArray *imageNameStrings = [NSMutableArray array];
NSString *appImageName = [FLEXUtility applicationImageName];
for (int i = 0; i < imageNamesCount; i++) {
for (unsigned int i = 0; i < imageNamesCount; i++) {
const char *imageName = imageNames[i];
NSString *imageNameString = [NSString stringWithUTF8String:imageName];
// Skip the app's image. We're just showing system libraries and frameworks.
@@ -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);
@@ -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];
@@ -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,237 @@
//
// 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 () <UISearchDisplayDelegate>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@property (nonatomic, strong) UISearchDisplayController *searchController;
#pragma clang diagnostic pop
@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)];
UISearchBar *searchBar = [[UISearchBar alloc] init];
[searchBar sizeToFit];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
#pragma clang diagnostic pop
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
[self.searchController.searchResultsTableView registerClass:[FLEXSystemLogTableViewCell class] forCellReuseIdentifier:kFLEXSystemLogTableViewCellIdentifier];
self.searchController.searchResultsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
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
{
NSInteger numberOfRows = 0;
if (tableView == self.tableView) {
numberOfRows = [self.logMessages count];
} else if (tableView == self.searchController.searchResultsTableView) {
numberOfRows = [self.filteredLogMessages count];
}
return numberOfRows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXSystemLogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXSystemLogTableViewCellIdentifier forIndexPath:indexPath];
if (tableView == self.tableView) {
cell.logMessage = [self.logMessages objectAtIndex:indexPath.row];
cell.highlightedText = nil;
} else if (tableView == self.searchController.searchResultsTableView) {
cell.logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
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 = nil;
if (tableView == self.tableView) {
logMessage = [self.logMessages objectAtIndex:indexPath.row];
} else if (tableView == self.searchController.searchResultsTableView) {
logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
}
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 = nil;
if (tableView == self.tableView) {
logMessage = [self.logMessages objectAtIndex:indexPath.row];
} else if (tableView == self.searchController.searchResultsTableView) {
logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
}
NSString *stringToCopy = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage] ?: @"";
[[UIPasteboard generalPasteboard] setString:stringToCopy];
}
}
#pragma mark - Search display delegate
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
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 ([self.searchDisplayController.searchBar.text isEqual:searchString]) {
self.filteredLogMessages = filteredLogMessages;
[self.searchDisplayController.searchResultsTableView reloadData];
}
});
});
// Reload done after the data fetches asynchronously
return NO;
}
#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,372 @@
//
// 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 () <UISearchDisplayDelegate>
/// 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;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@property (nonatomic, strong) UISearchDisplayController *searchController;
#pragma clang diagnostic pop
@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];
UISearchBar *searchBar = [[UISearchBar alloc] init];
[searchBar sizeToFit];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
#pragma clang diagnostic pop
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
[self.searchController.searchResultsTableView registerClass:[FLEXNetworkTransactionTableViewCell class] forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier];
self.searchController.searchResultsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.searchController.searchResultsTableView.rowHeight = [FLEXNetworkTransactionTableViewCell preferredCellHeight];
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 updateFirstSectionHeaderInTableView:self.tableView];
}
- (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 updateFirstSectionHeaderInTableView:self.searchController.searchResultsTableView];
}
- (void)updateFirstSectionHeaderInTableView:(UITableView *)tableView
{
UIView *view = [tableView headerViewForSection:0];
if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.textLabel.text = [self headerTextForTableView:tableView];
[headerView setNeedsLayout];
}
}
- (NSString *)headerTextForTableView:(UITableView *)tableView
{
NSString *headerText = nil;
if ([FLEXNetworkObserver isEnabled]) {
long long bytesReceived = 0;
NSInteger totalRequests = 0;
if (tableView == self.tableView) {
bytesReceived = self.bytesReceived;
totalRequests = [self.networkTransactions count];
} else if (tableView == self.searchController.searchResultsTableView) {
bytesReceived = self.filteredBytesReceived;
totalRequests = [self.filteredNetworkTransactions 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;
}
NSInteger existingRowCount = [self.networkTransactions count];
[self updateTransactions];
NSInteger newRowCount = [self.networkTransactions count];
NSInteger addedRowCount = newRowCount - existingRowCount;
if (addedRowCount != 0) {
// 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);
}
if (self.searchController.isActive) {
[self updateSearchResultsWithSearchString:self.searchController.searchBar.text];
}
}
}
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
{
[self updateBytesReceived];
[self updateFilteredBytesReceived];
FLEXNetworkTransaction *transaction = [notification.userInfo objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
NSArray *tableViews = @[self.tableView];
if (self.searchController.searchResultsTableView) {
tableViews = [tableViews arrayByAddingObject:self.searchController.searchResultsTableView];
}
// Update both the main table view and search table view if needed.
for (UITableView *tableView in tableViews) {
for (FLEXNetworkTransactionTableViewCell *cell in [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 updateFirstSectionHeaderInTableView:tableView];
}
}
- (void)handleTransactionsClearedNotification:(NSNotification *)notification
{
[self updateTransactions];
[self.tableView reloadData];
[self.searchController.searchResultsTableView reloadData];
}
- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification
{
// Update the header, which displays a warning when network debugging is disabled
[self updateFirstSectionHeaderInTableView:self.tableView];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger numberOfRows = 0;
if (tableView == self.tableView) {
numberOfRows = [self.networkTransactions count];
} else if (tableView == self.searchController.searchResultsTableView) {
numberOfRows = [self.filteredNetworkTransactions count];
}
return numberOfRows;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [self headerTextForTableView:tableView];
}
- (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
{
FLEXNetworkTransaction *transaction = nil;
if (tableView == self.tableView) {
transaction = [self.networkTransactions objectAtIndex:indexPath.row];
} else if (tableView == self.searchController.searchResultsTableView) {
transaction = [self.filteredNetworkTransactions objectAtIndex:indexPath.row];
}
return transaction;
}
#pragma mark - Search display delegate
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[self updateSearchResultsWithSearchString:searchString];
// Reload done after the data is filtered asynchronously
return NO;
}
- (void)updateSearchResultsWithSearchString:(NSString *)searchString
{
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.searchController.searchResultsTableView 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 objectForKey: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 objectForKey: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 objectForKey: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 objectForKey: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 objectForKey: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,194 @@
//
// 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 *enableOnLaunchCell = [self switchCellWithTitle:@"Enable on Launch" toggleAction:@selector(enableOnLaunchToggled:) isOn:[FLEXNetworkObserver shouldEnableOnLaunch]];
[mutableCells addObject:enableOnLaunchCell];
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)enableOnLaunchToggled:(UISwitch *)sender
{
[FLEXNetworkObserver setShouldEnableOnLaunch: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 objectAtIndex: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 objectAtIndex:section];
return [sectionModel.rows count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex: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 objectAtIndex:indexPath.section];
return [sectionModel.rows objectAtIndex: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 objectForKey: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,178 @@
//
// 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];
[detailComponents addObject:timestamp];
// Omit method for GET (assumed as default)
NSString *httpMethod = self.transaction.request.HTTPMethod;
if (httpMethod) {
[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,33 @@
//
// 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.
+ (void)setEnabled:(BOOL)enabled;
+ (BOOL)isEnabled;
/// The enable on launch setting is persisted accross launches of the app.
/// If YES, the observer will automatically enable itself early in the application lifecycle.
+ (void)setShouldEnableOnLaunch:(BOOL)shouldEnableOnLaunch;
+ (BOOL)shouldEnableOnLaunch;
@end
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
PonyDebugger
Copyright 2012 Square Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,15 +0,0 @@
//
// FLEXDescriptionTableViewCell.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-05.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXDescriptionTableViewCell : UITableViewCell
+ (CGFloat)preferredHeightWithText:(NSString *)text inTableViewWidth:(CGFloat)tableViewWidth;
@end
@@ -1,85 +0,0 @@
//
// FLEXDescriptionTableViewCell.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-05.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXDescriptionTableViewCell.h"
#import "FLEXUtility.h"
@interface FLEXDescriptionTableViewCell ()
@end
@implementation FLEXDescriptionTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.textLabel.numberOfLines = 0;
self.textLabel.font = [[self class] labelFont];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.textLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, [[self class] labelInsets]);
}
+ (UIFont *)labelFont
{
return [FLEXUtility defaultTableViewCellLabelFont];
}
+ (UIEdgeInsets)labelInsets
{
UIEdgeInsets labelInsets = UIEdgeInsetsZero;
labelInsets.top = 15.0;
labelInsets.bottom = 15.0;
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
labelInsets.left = 15.0;
labelInsets.right = 0.0;
} else {
labelInsets.left = 10.0;
labelInsets.right = 10.0;
}
return labelInsets;
}
+ (CGFloat)preferredHeightWithText:(NSString *)text inTableViewWidth:(CGFloat)tableViewWidth
{
// Hardcoded margins from observation of cells in a grouped table on iOS 6.
// There is no API to get the insets of the content view proir to layout.
// Thankfully they removed the magic margins in iOS 7.
// Differences are between the content view's width and the table view's width
// Full screen iPhone - 20
// Full screen iPad - 90
CGFloat labelWidth = tableViewWidth;
if (NSFoundationVersionNumber <= NSFoundationVersionNumber_iOS_6_1) {
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
labelWidth -= 40.0;
} else {
labelWidth -= 90.0;
}
}
UIEdgeInsets labelInsets = [self labelInsets];
labelWidth -= (labelInsets.left + labelInsets.right);
// Size an attributed string to get around deprecation warnings if the deployment target is >= 7 while still supporting deployment tagets back to 6.0.
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: [self labelFont]}];
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
CGFloat preferredLabelHeight = ceil([attributedText boundingRectWithSize:constrainSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size.height);
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
return preferredCellHeight;
}
@end
@@ -0,0 +1,22 @@
//
// FLEXGlobalsTableViewControllerEntry.h
// UICatalog
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NSString *(^FLEXGlobalsTableViewControllerEntryNameFuture)(void);
typedef UIViewController *(^FLEXGlobalsTableViewControllerViewControllerFuture)(void);
@interface FLEXGlobalsTableViewControllerEntry : NSObject
@property (nonatomic, readonly, copy) FLEXGlobalsTableViewControllerEntryNameFuture entryNameFuture;
@property (nonatomic, readonly, copy) FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsTableViewControllerEntryNameFuture)nameFuture viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture;
@end
@@ -0,0 +1,25 @@
//
// FLEXGlobalsTableViewControllerEntry.m
// UICatalog
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXGlobalsTableViewControllerEntry.h"
@implementation FLEXGlobalsTableViewControllerEntry
+ (instancetype)entryWithNameFuture:(FLEXGlobalsTableViewControllerEntryNameFuture)nameFuture viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture
{
NSParameterAssert(nameFuture);
NSParameterAssert(viewControllerFuture);
FLEXGlobalsTableViewControllerEntry *entry = [[self alloc] init];
entry->_entryNameFuture = [nameFuture copy];
entry->_viewControllerFuture = [viewControllerFuture copy];
return entry;
}
@end

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