Compare commits

...

156 Commits

Author SHA1 Message Date
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
65 changed files with 4755 additions and 232 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
...
@@ -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
@@ -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];
}
@@ -47,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];
@@ -147,11 +147,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
if (viewControllerToAsk && viewControllerToAsk != self) {
// We might need to foward to a child
UIViewController *childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarStyle];
if (childViewControllerToAsk) {
preferredStyle = [childViewControllerToAsk preferredStatusBarStyle];
} else {
preferredStyle = [viewControllerToAsk preferredStatusBarStyle];
while (childViewControllerToAsk && childViewControllerToAsk != viewControllerToAsk) {
viewControllerToAsk = childViewControllerToAsk;
childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarStyle];
}
preferredStyle = [viewControllerToAsk preferredStatusBarStyle];
}
return preferredStyle;
}
@@ -173,11 +174,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
if (viewControllerToAsk && viewControllerToAsk != self) {
// Again, we might need to forward to a child
UIViewController *childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarHidden];
if (childViewControllerToAsk) {
prefersHidden = [childViewControllerToAsk prefersStatusBarHidden];
} else {
prefersHidden = [viewControllerToAsk prefersStatusBarHidden];
while (childViewControllerToAsk && childViewControllerToAsk != viewControllerToAsk) {
viewControllerToAsk = childViewControllerToAsk;
childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarHidden];
}
prefersHidden = [viewControllerToAsk prefersStatusBarHidden];
}
return prefersHidden;
}
@@ -447,20 +449,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;
}
@@ -592,7 +597,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];
}
}
@@ -842,11 +851,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
// for view controller based management because the global methods no-op if that is turned on.
// Only for iOS 7+
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
}
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
@@ -863,10 +869,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
// Restore the stauts bar style if the app is using global status bar management.
// Only for iOS 7+
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
}
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
[self dismissViewControllerAnimated:animated completion:completion];
}
+19 -1
View File
@@ -17,6 +17,15 @@
- (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.
@@ -26,6 +35,15 @@
/// @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;
- (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
+39 -2
View File
@@ -12,6 +12,8 @@
#import "FLEXGlobalsTableViewControllerEntry.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
@@ -50,9 +52,7 @@
if (!_explorerWindow) {
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
_explorerWindow.eventDelegate = self;
_explorerWindow.rootViewController = self.explorerViewController;
[_explorerWindow addSubview:self.explorerViewController.view];
}
return _explorerWindow;
@@ -83,6 +83,25 @@
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
@@ -119,4 +138,22 @@
[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
@@ -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,137 @@
//
// FLEXFileBrowserFileOperationController.m
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/13/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXFileBrowserFileOperationController.h"
@interface FLEXFileBrowserFileDeleteOperationController () <UIAlertViewDelegate>
@property (nonatomic, copy, readonly) NSString *path;
@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;
@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
@@ -7,20 +7,29 @@
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXFileBrowserFileOperationController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
@interface FLEXFileBrowserTableViewController ()
@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
@@ -42,7 +51,10 @@
//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;
@@ -71,12 +83,23 @@
[strongSelf.tableView reloadData];
});
});
self.childPaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
[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
@@ -90,14 +113,8 @@
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
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:searchString];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
self.searchString = searchString;
[self reloadSearchPaths];
return YES;
}
@@ -106,6 +123,8 @@
{
//confirm to clear all operations
[self.operationQueue cancelAllOperations];
[self reloadChildPaths];
[self.tableView reloadData];
}
@@ -178,7 +197,7 @@
NSString *cellIdentifier = showImagePreview ? imageCellIdentifier : textCellIdentifier;
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
@@ -224,9 +243,7 @@
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];
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];
@@ -253,16 +270,122 @@
}
} 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.searchDisplayController.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.searchDisplayController.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.searchDisplayController.isActive) {
[self reloadSearchPaths];
[self.searchDisplayController.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,10 +16,14 @@
#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,
@@ -166,6 +170,24 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
return [[FLEXFileBrowserTableViewController alloc] init];
};
break;
case FLEXGlobalsRowSystemLog:
titleFuture = ^{
return @"⚠️ System Log";
};
viewControllerFuture = ^{
return [[FLEXSystemLogTableViewController alloc] init];
};
break;
case FLEXGlobalsRowNetworkHistory:
titleFuture = ^{
return @"📡 Network History";
};
viewControllerFuture = ^{
return [[FLEXNetworkHistoryTableViewController alloc] init];
};
break;
case FLEXGlobalsRowCount:
break;
}
@@ -183,7 +205,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
{
self = [super initWithStyle:style];
if (self) {
self.title = @"🌎 Global State";
self.title = @"💪 FLEX";
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
}
return self;
@@ -252,7 +274,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
@@ -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
@@ -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,369 @@
//
// 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];
self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
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]) {
NSIndexPath *indexPath = [tableView indexPathForCell:cell];
if (indexPath) {
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
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.searchDisplayController.searchBar.text isEqual:searchString]) {
self.filteredNetworkTransactions = filteredNetworkTransactions;
[self.searchDisplayController.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
+255
View File
@@ -0,0 +1,255 @@
//
// 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
{
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 = [NSDate date];
[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
{
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
if (!transaction) {
return;
}
transaction.response = response;
transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
transaction.latency = -[transaction.startTime timeIntervalSinceNow];
[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
{
dispatch_async(self.queue, ^{
FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
if (!transaction) {
return;
}
transaction.transactionState = FLEXNetworkTransactionStateFinished;
transaction.duration = -[transaction.startTime timeIntervalSinceNow];
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/"]) {
// 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
+40
View File
@@ -0,0 +1,40 @@
//
// FLEXNetworkTransaction.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.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,438 @@
//
// 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];
}
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];
}
}
#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 - 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 ([mimeType isEqual:@"application/json"] || [mimeType isEqual:@"application/javascript"] || [mimeType isEqual:@"text/javascript"]) {
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,31 @@
//
// 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.
//
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,13 @@
//
// FLEXLayerExplorerViewController.h
// UICatalog
//
// Created by Ryan Olson on 12/14/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXObjectExplorerViewController.h"
@interface FLEXLayerExplorerViewController : FLEXObjectExplorerViewController
@end
@@ -0,0 +1,92 @@
//
// FLEXLayerExplorerViewController.m
// UICatalog
//
// Created by Ryan Olson on 12/14/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXLayerExplorerViewController.h"
#import "FLEXImagePreviewViewController.h"
typedef NS_ENUM(NSUInteger, FLEXLayerExplorerRow) {
FLEXLayerExplorerRowPreview
};
@interface FLEXLayerExplorerViewController ()
@property (nonatomic, readonly) CALayer *layerToExplore;
@end
@implementation FLEXLayerExplorerViewController
- (CALayer *)layerToExplore
{
return [self.object isKindOfClass:[CALayer class]] ? self.object : nil;
}
#pragma mark - Superclass Overrides
- (NSString *)customSectionTitle
{
return @"Shortcuts";
}
- (NSArray *)customSectionRowCookies
{
return @[@(FLEXLayerExplorerRowPreview)];
}
- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
{
NSString *title = nil;
if ([rowCookie isKindOfClass:[NSNumber class]]) {
FLEXLayerExplorerRow row = [rowCookie unsignedIntegerValue];
switch (row) {
case FLEXLayerExplorerRowPreview:
title = @"Preview Image";
break;
}
}
return title;
}
- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
{
return YES;
}
- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
{
UIViewController *drillInViewController = nil;
if ([rowCookie isKindOfClass:[NSNumber class]]) {
FLEXLayerExplorerRow row = [rowCookie unsignedIntegerValue];
switch (row) {
case FLEXLayerExplorerRowPreview:
drillInViewController = [[self class] imagePreviewViewControllerForLayer:self.layerToExplore];
break;
}
}
return drillInViewController;
}
+ (UIViewController *)imagePreviewViewControllerForLayer:(CALayer *)layer
{
UIViewController *imagePreviewViewController = nil;
if (!CGRectIsEmpty(layer.bounds)) {
UIGraphicsBeginImageContextWithOptions(layer.bounds.size, NO, 0.0);
CGContextRef imageContext = UIGraphicsGetCurrentContext();
[layer renderInContext:imageContext];
UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
imagePreviewViewController = [[FLEXImagePreviewViewController alloc] initWithImage:previewImage];
}
return imagePreviewViewController;
}
@end
@@ -16,6 +16,7 @@
#import "FLEXViewExplorerViewController.h"
#import "FLEXImageExplorerViewController.h"
#import "FLEXClassExplorerViewController.h"
#import "FLEXLayerExplorerViewController.h"
#import <objc/runtime.h>
@implementation FLEXObjectExplorerFactory
@@ -36,7 +37,8 @@
NSStringFromClass([NSUserDefaults class]) : [FLEXDefaultsExplorerViewController class],
NSStringFromClass([UIViewController class]) : [FLEXViewControllerExplorerViewController class],
NSStringFromClass([UIView class]) : [FLEXViewExplorerViewController class],
NSStringFromClass([UIImage class]) : [FLEXImageExplorerViewController class]};
NSStringFromClass([UIImage class]) : [FLEXImageExplorerViewController class],
NSStringFromClass([CALayer class]) : [FLEXLayerExplorerViewController class]};
});
Class explorerClass = nil;
@@ -9,7 +9,7 @@
#import "FLEXObjectExplorerViewController.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXDescriptionTableViewCell.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXPropertyEditorViewController.h"
#import "FLEXIvarEditorViewController.h"
@@ -708,6 +708,21 @@ static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
return canDrillIn;
}
- (BOOL)canCopyRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
{
BOOL canCopy = NO;
switch (section) {
case FLEXObjectExplorerSectionDescription:
canCopy = YES;
break;
default:
break;
}
return canCopy;
}
- (NSString *)titleForExplorerSection:(FLEXObjectExplorerSection)section
{
NSString *title = nil;
@@ -845,11 +860,12 @@ static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
NSString *cellIdentifier = useDescriptionCell ? @"descriptionCell" : @"cell";
NSString *cellIdentifier = useDescriptionCell ? kFLEXMultilineTableViewCellIdentifier : @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
if (useDescriptionCell) {
cell = [[FLEXDescriptionTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell = [[FLEXMultilineTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
} else {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
@@ -872,7 +888,8 @@ static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
CGFloat height = self.tableView.rowHeight;
if (explorerSection == FLEXObjectExplorerSectionDescription) {
NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
CGFloat preferredHeight = [FLEXDescriptionTableViewCell preferredHeightWithText:text inTableViewWidth:self.tableView.frame.size.width];
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont] }];
CGFloat preferredHeight = [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.frame.size.width style:tableView.style showsAccessory:NO];
height = MAX(height, preferredHeight);
}
return height;
@@ -898,6 +915,49 @@ static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
BOOL canCopy = [self canCopyRow:indexPath.row inExplorerSection:explorerSection];
return canCopy;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
BOOL canPerformAction = NO;
if (action == @selector(copy:)) {
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
BOOL canCopy = [self canCopyRow:indexPath.row inExplorerSection:explorerSection];
canPerformAction = canCopy;
}
return canPerformAction;
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
NSString *stringToCopy = @"";
NSString *title = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
if ([title length] > 0) {
stringToCopy = [stringToCopy stringByAppendingString:title];
}
NSString *subtitle = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
if ([subtitle length] > 0) {
if ([stringToCopy length] > 0) {
stringToCopy = [stringToCopy stringByAppendingString:@"\n\n"];
}
stringToCopy = [stringToCopy stringByAppendingString:subtitle];
}
[[UIPasteboard generalPasteboard] setString:stringToCopy];
}
}
#pragma mark - Custom Section
@@ -163,12 +163,7 @@ typedef NS_ENUM(NSUInteger, FLEXViewExplorerRow) {
if (!CGRectIsEmpty(view.bounds)) {
CGSize viewSize = view.bounds.size;
UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0);
if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
[view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
} else {
CGContextRef imageContext = UIGraphicsGetCurrentContext();
[view.layer renderInContext:imageContext];
}
[view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
imagePreviewViewController = [[FLEXImagePreviewViewController alloc] initWithImage:previewImage];
@@ -0,0 +1,17 @@
//
// FLEXMultilineTableViewCell.h
// UICatalog
//
// Created by Ryan Olson on 2/13/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import <UIKit/UIKit.h>
extern NSString *const kFLEXMultilineTableViewCellIdentifier;
@interface FLEXMultilineTableViewCell : UITableViewCell
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText inTableViewWidth:(CGFloat)tableViewWidth style:(UITableViewStyle)style showsAccessory:(BOOL)showsAccessory;
@end
@@ -0,0 +1,55 @@
//
// FLEXMultilineTableViewCell.m
// UICatalog
//
// Created by Ryan Olson on 2/13/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXMultilineTableViewCell.h"
NSString *const kFLEXMultilineTableViewCellIdentifier = @"kFLEXMultilineTableViewCellIdentifier";
@implementation FLEXMultilineTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.textLabel.numberOfLines = 0;
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.textLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, [[self class] labelInsets]);
}
+ (UIEdgeInsets)labelInsets
{
return UIEdgeInsetsMake(10.0, 15.0, 10.0, 15.0);
}
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText inTableViewWidth:(CGFloat)tableViewWidth style:(UITableViewStyle)style showsAccessory:(BOOL)showsAccessory
{
CGFloat labelWidth = tableViewWidth;
// Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
if (showsAccessory) {
labelWidth -= 34.0;
}
UIEdgeInsets labelInsets = [self labelInsets];
labelWidth -= (labelInsets.left + labelInsets.right);
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
CGFloat preferredLabelHeight = ceil([attributedText boundingRectWithSize:constrainSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size.height);
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
return preferredCellHeight;
}
@end
+11
View File
@@ -19,4 +19,15 @@
+ (UIImage *)moveIcon;
+ (UIImage *)selectIcon;
+ (UIImage *)jsonIcon;
+ (UIImage *)textPlainIcon;
+ (UIImage *)htmlIcon;
+ (UIImage *)audioIcon;
+ (UIImage *)jsIcon;
+ (UIImage *)plistIcon;
+ (UIImage *)textIcon;
+ (UIImage *)videoIcon;
+ (UIImage *)xmlIcon;
+ (UIImage *)binaryIcon;
@end
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -148,6 +148,10 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
BOOL boolValue = NO;
[value getValue:&boolValue];
description = boolValue ? @"YES" : @"NO";
} else if (strcmp(type, @encode(SEL)) == 0) {
SEL selector = NULL;
[value getValue:&selector];
description = NSStringFromSelector(selector);
}
}
@@ -317,7 +321,7 @@ const unsigned int kFLEXNumberOfImplicitArgs = 2;
// Bridging UIColor to CGColorRef
CGColorRef colorRef = [argumentObject CGColor];
[invocation setArgument:&colorRef atIndex:argumentIndex];
} else if ([argumentObject isKindOfClass:[NSValue class]]){
} else if ([argumentObject isKindOfClass:[NSValue class]]) {
// Primitive boxed in NSValue
NSValue *argumentValue = (NSValue *)argumentObject;
+6
View File
@@ -30,5 +30,11 @@
+ (UIInterfaceOrientationMask)infoPlistSupportedInterfaceOrientationsMask;
+ (NSString *)searchBarPlaceholderText;
+ (BOOL)isImagePathExtension:(NSString *)extension;
+ (UIImage *)thumbnailedImageWithMaxPixelDimension:(NSInteger)dimension fromImageData:(NSData *)data;
+ (NSString *)stringFromRequestDuration:(NSTimeInterval)duration;
+ (NSString *)statusCodeStringFromURLResponse:(NSURLResponse *)response;
+ (NSDictionary *)dictionaryFromQuery:(NSString *)query;
+ (NSString *)prettyJSONStringFromData:(NSData *)data;
+ (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData;
@end
+138 -1
View File
@@ -8,6 +8,8 @@
#import "FLEXUtility.h"
#import "FLEXResources.h"
#import <ImageIO/ImageIO.h>
#import <zlib.h>
@implementation FLEXUtility
@@ -90,7 +92,7 @@
+ (NSString *)applicationImageName
{
return [[[NSProcessInfo processInfo] arguments] objectAtIndex:0];
return [[NSBundle mainBundle] executablePath];
}
+ (NSString *)applicationName
@@ -183,4 +185,139 @@
return [@[@"jpg", @"jpeg", @"png", @"gif", @"tiff", @"tif"] containsObject:extension];
}
+ (UIImage *)thumbnailedImageWithMaxPixelDimension:(NSInteger)dimension fromImageData:(NSData *)data
{
UIImage *thumbnail = nil;
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, 0);
if (imageSource) {
NSDictionary *options = @{ (__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(dimension) };
CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
if (scaledImageRef) {
thumbnail = [UIImage imageWithCGImage:scaledImageRef];
CFRelease(scaledImageRef);
}
CFRelease(imageSource);
}
return thumbnail;
}
+ (NSString *)stringFromRequestDuration:(NSTimeInterval)duration
{
NSString *string = @"0s";
if (duration > 0.0) {
if (duration < 1.0) {
string = [NSString stringWithFormat:@"%dms", (int)(duration * 1000)];
} else if (duration < 10.0) {
string = [NSString stringWithFormat:@"%.2fs", duration];
} else {
string = [NSString stringWithFormat:@"%.1fs", duration];
}
}
return string;
}
+ (NSString *)statusCodeStringFromURLResponse:(NSURLResponse *)response
{
NSString *httpResponseString = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSString *statusCodeDescription = nil;
if (httpResponse.statusCode == 200) {
// Prefer OK to the default "no error"
statusCodeDescription = @"OK";
} else {
statusCodeDescription = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
}
httpResponseString = [NSString stringWithFormat:@"%ld %@", (long)httpResponse.statusCode, statusCodeDescription];
}
return httpResponseString;
}
+ (NSDictionary *)dictionaryFromQuery:(NSString *)query
{
NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionary];
// [a=1, b=2, c=3]
NSArray *queryComponents = [query componentsSeparatedByString:@"&"];
for (NSString *keyValueString in queryComponents) {
// [a, 1]
NSArray *components = [keyValueString componentsSeparatedByString:@"="];
if ([components count] == 2) {
NSString *key = [[components firstObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
id value = [[components lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// Handle multiple entries under the same key as an array
id existingEntry = [queryDictionary objectForKey:key];
if (existingEntry) {
if ([existingEntry isKindOfClass:[NSArray class]]) {
value = [existingEntry arrayByAddingObject:value];
} else {
value = @[existingEntry, value];
}
}
[queryDictionary setObject:value forKey:key];
}
}
return queryDictionary;
}
+ (NSString *)prettyJSONStringFromData:(NSData *)data
{
NSString *prettyString = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
if ([NSJSONSerialization isValidJSONObject:jsonObject]) {
prettyString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
// NSJSONSerialization escapes forward slashes. We want pretty json, so run through and unescape the slashes.
prettyString = [prettyString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
} else {
prettyString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
return prettyString;
}
// Thanks to the following links for help with this method
// http://www.cocoanetics.com/2012/02/decompressing-files-into-memory/
// https://github.com/nicklockwood/GZIP
+ (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData
{
NSData *inflatedData = nil;
NSUInteger compressedDataLength = [compressedData length];
if (compressedDataLength > 0) {
z_stream stream;
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.avail_in = (uInt)compressedDataLength;
stream.next_in = (void *)[compressedData bytes];
stream.total_out = 0;
stream.avail_out = 0;
NSMutableData *mutableData = [NSMutableData dataWithLength:compressedDataLength * 1.5];
if (inflateInit2(&stream, 15 + 32) == Z_OK) {
int status = Z_OK;
while (status == Z_OK) {
if (stream.total_out >= [mutableData length]) {
mutableData.length += compressedDataLength / 2;
}
stream.next_out = (uint8_t *)[mutableData mutableBytes] + stream.total_out;
stream.avail_out = (uInt)([mutableData length] - stream.total_out);
status = inflate(&stream, Z_SYNC_FLUSH);
}
if (inflateEnd(&stream) == Z_OK) {
if (status == Z_STREAM_END) {
mutableData.length = stream.total_out;
inflatedData = [mutableData copy];
}
}
}
}
return inflatedData;
}
@end
@@ -37,13 +37,7 @@
self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14.0];
self.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
// Supported starting with iOS 7
self.accessoryType = UITableViewCellAccessoryDetailButton;
} else {
self.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
}
self.accessoryType = UITableViewCellAccessoryDetailButton;
}
return self;
}
@@ -56,9 +56,7 @@ static const NSInteger kFLEXHierarchyScopeFullHierarchyIndex = 1;
self.tableView.rowHeight = 50.0;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
// Separator inset clashes with persistent cell selection.
if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
+119 -8
View File
@@ -43,11 +43,19 @@
535682BE18F3670300BAAD62 /* AAPLWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 535682A418F3670300BAAD62 /* AAPLWebViewController.m */; };
535682BF18F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.m in Sources */ = {isa = PBXBuildFile; fileRef = 535682A618F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.m */; };
53874F9918F36B1800510922 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53874F9718F36B1800510922 /* Localizable.strings */; };
650855EB1A9007D5006109A1 /* FLEXArgumentInputDateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */; };
65F8DC6C1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */; };
9421B88D1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8801A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m */; };
9421B88E1A8BBCB200BA3E46 /* FLEXNetworkRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8821A8BBCB200BA3E46 /* FLEXNetworkRecorder.m */; };
9421B88F1A8BBCB200BA3E46 /* FLEXNetworkTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8841A8BBCB200BA3E46 /* FLEXNetworkTransaction.m */; };
9421B8901A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8861A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m */; };
9421B8911A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */; };
9421B8921A8BBCB200BA3E46 /* FLEXNetworkObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */; };
9421B8931A8BBCB200BA3E46 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9421B88C1A8BBCB200BA3E46 /* LICENSE */; };
943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */; };
944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7426197B458C009AB039 /* FLEXArrayExplorerViewController.m */; };
944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */; };
944F748B197B458C009AB039 /* FLEXDefaultsExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */; };
944F748C197B458C009AB039 /* FLEXDescriptionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742C197B458C009AB039 /* FLEXDescriptionTableViewCell.m */; };
944F748D197B458C009AB039 /* FLEXDictionaryExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */; };
944F748E197B458C009AB039 /* FLEXImageExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7430197B458C009AB039 /* FLEXImageExplorerViewController.m */; };
944F748F197B458C009AB039 /* FLEXObjectExplorerFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7432197B458C009AB039 /* FLEXObjectExplorerFactory.m */; };
@@ -91,6 +99,13 @@
944F74B5197B458C009AB039 /* FLEXHierarchyTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7484197B458C009AB039 /* FLEXHierarchyTableViewCell.m */; };
944F74B6197B458C009AB039 /* FLEXHierarchyTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7486197B458C009AB039 /* FLEXHierarchyTableViewController.m */; };
944F74B7197B458C009AB039 /* FLEXImagePreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7488197B458C009AB039 /* FLEXImagePreviewViewController.m */; };
946C6EC91A7598D3006545C2 /* FLEXSystemLogTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6EC81A7598D3006545C2 /* FLEXSystemLogTableViewCell.m */; };
946C6ECC1A759928006545C2 /* FLEXSystemLogTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6ECA1A759928006545C2 /* FLEXSystemLogTableViewController.m */; };
946C6ECF1A7599C4006545C2 /* FLEXSystemLogMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */; };
94C681F31A3E941800E1936D /* FLEXLayerExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */; };
94CB48391A8EC6000054A905 /* FLEXMultilineTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */; };
94CB4D431A97183E0054A905 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CB4D421A97183E0054A905 /* libz.dylib */; };
94CB4D4F1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CB4D4E1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m */; };
D03647D919847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = D03647D819847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m */; };
/* End PBXBuildFile section */
@@ -161,6 +176,23 @@
535682A518F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+AAPLApplicationSpecific.h"; sourceTree = "<group>"; };
535682A618F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+AAPLApplicationSpecific.m"; sourceTree = "<group>"; };
53874F9818F36B1800510922 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
650855E91A9007D5006109A1 /* FLEXArgumentInputDateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXArgumentInputDateView.h; sourceTree = "<group>"; };
650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXArgumentInputDateView.m; sourceTree = "<group>"; };
65F8DC6A1A8F11020076F87B /* FLEXFileBrowserFileOperationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXFileBrowserFileOperationController.h; sourceTree = "<group>"; };
65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXFileBrowserFileOperationController.m; sourceTree = "<group>"; };
9421B87F1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkHistoryTableViewController.h; sourceTree = "<group>"; };
9421B8801A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkHistoryTableViewController.m; sourceTree = "<group>"; };
9421B8811A8BBCB200BA3E46 /* FLEXNetworkRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkRecorder.h; sourceTree = "<group>"; };
9421B8821A8BBCB200BA3E46 /* FLEXNetworkRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkRecorder.m; sourceTree = "<group>"; };
9421B8831A8BBCB200BA3E46 /* FLEXNetworkTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransaction.h; sourceTree = "<group>"; };
9421B8841A8BBCB200BA3E46 /* FLEXNetworkTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransaction.m; sourceTree = "<group>"; };
9421B8851A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransactionDetailTableViewController.h; sourceTree = "<group>"; };
9421B8861A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransactionDetailTableViewController.m; sourceTree = "<group>"; };
9421B8871A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransactionTableViewCell.h; sourceTree = "<group>"; };
9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransactionTableViewCell.m; sourceTree = "<group>"; };
9421B88A1A8BBCB200BA3E46 /* FLEXNetworkObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkObserver.h; sourceTree = "<group>"; };
9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkObserver.m; sourceTree = "<group>"; };
9421B88C1A8BBCB200BA3E46 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
943203FC1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLCatalogTableTableViewController.h; sourceTree = "<group>"; };
943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLCatalogTableTableViewController.m; sourceTree = "<group>"; };
944F7425197B458C009AB039 /* FLEXArrayExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXArrayExplorerViewController.h; sourceTree = "<group>"; };
@@ -169,8 +201,6 @@
944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXClassExplorerViewController.m; sourceTree = "<group>"; };
944F7429197B458C009AB039 /* FLEXDefaultsExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDefaultsExplorerViewController.h; sourceTree = "<group>"; };
944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDefaultsExplorerViewController.m; sourceTree = "<group>"; };
944F742B197B458C009AB039 /* FLEXDescriptionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDescriptionTableViewCell.h; sourceTree = "<group>"; };
944F742C197B458C009AB039 /* FLEXDescriptionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDescriptionTableViewCell.m; sourceTree = "<group>"; };
944F742D197B458C009AB039 /* FLEXDictionaryExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDictionaryExplorerViewController.h; sourceTree = "<group>"; };
944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDictionaryExplorerViewController.m; sourceTree = "<group>"; };
944F742F197B458C009AB039 /* FLEXImageExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXImageExplorerViewController.h; sourceTree = "<group>"; };
@@ -257,6 +287,19 @@
944F7486197B458C009AB039 /* FLEXHierarchyTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXHierarchyTableViewController.m; sourceTree = "<group>"; };
944F7487197B458C009AB039 /* FLEXImagePreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXImagePreviewViewController.h; sourceTree = "<group>"; };
944F7488197B458C009AB039 /* FLEXImagePreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXImagePreviewViewController.m; sourceTree = "<group>"; };
946C6EC71A7598D3006545C2 /* FLEXSystemLogTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLEXSystemLogTableViewCell.h; path = "System Log/FLEXSystemLogTableViewCell.h"; sourceTree = "<group>"; };
946C6EC81A7598D3006545C2 /* FLEXSystemLogTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLEXSystemLogTableViewCell.m; path = "System Log/FLEXSystemLogTableViewCell.m"; sourceTree = "<group>"; };
946C6ECA1A759928006545C2 /* FLEXSystemLogTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLEXSystemLogTableViewController.m; path = "System Log/FLEXSystemLogTableViewController.m"; sourceTree = "<group>"; };
946C6ECB1A759928006545C2 /* FLEXSystemLogTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLEXSystemLogTableViewController.h; path = "System Log/FLEXSystemLogTableViewController.h"; sourceTree = "<group>"; };
946C6ECD1A7599C4006545C2 /* FLEXSystemLogMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLEXSystemLogMessage.h; path = "System Log/FLEXSystemLogMessage.h"; sourceTree = "<group>"; };
946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLEXSystemLogMessage.m; path = "System Log/FLEXSystemLogMessage.m"; sourceTree = "<group>"; };
94C681F11A3E941800E1936D /* FLEXLayerExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXLayerExplorerViewController.h; sourceTree = "<group>"; };
94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXLayerExplorerViewController.m; sourceTree = "<group>"; };
94CB48371A8EC6000054A905 /* FLEXMultilineTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXMultilineTableViewCell.h; sourceTree = "<group>"; };
94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXMultilineTableViewCell.m; sourceTree = "<group>"; };
94CB4D421A97183E0054A905 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
94CB4D4D1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkSettingsTableViewController.h; sourceTree = "<group>"; };
94CB4D4E1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkSettingsTableViewController.m; sourceTree = "<group>"; };
D03647D51984720F007D2A1B /* FLEXManager+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FLEXManager+Private.h"; sourceTree = "<group>"; };
D03647D719847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXGlobalsTableViewControllerEntry.h; sourceTree = "<group>"; };
D03647D819847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXGlobalsTableViewControllerEntry.m; sourceTree = "<group>"; };
@@ -267,6 +310,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
94CB4D431A97183E0054A905 /* libz.dylib in Frameworks */,
5356824018F3656900BAAD62 /* CoreGraphics.framework in Frameworks */,
5356824218F3656900BAAD62 /* UIKit.framework in Frameworks */,
5356823E18F3656900BAAD62 /* Foundation.framework in Frameworks */,
@@ -298,6 +342,7 @@
5356823C18F3656900BAAD62 /* Frameworks */ = {
isa = PBXGroup;
children = (
94CB4D421A97183E0054A905 /* libz.dylib */,
5356823D18F3656900BAAD62 /* Foundation.framework */,
5356823F18F3656900BAAD62 /* CoreGraphics.framework */,
5356824118F3656900BAAD62 /* UIKit.framework */,
@@ -402,6 +447,37 @@
name = Application;
sourceTree = "<group>";
};
9421B87E1A8BBCB200BA3E46 /* Network */ = {
isa = PBXGroup;
children = (
9421B87F1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.h */,
9421B8801A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m */,
94CB4D4D1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.h */,
94CB4D4E1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m */,
9421B8811A8BBCB200BA3E46 /* FLEXNetworkRecorder.h */,
9421B8821A8BBCB200BA3E46 /* FLEXNetworkRecorder.m */,
9421B8831A8BBCB200BA3E46 /* FLEXNetworkTransaction.h */,
9421B8841A8BBCB200BA3E46 /* FLEXNetworkTransaction.m */,
9421B8851A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.h */,
9421B8861A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m */,
9421B8871A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.h */,
9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */,
9421B8891A8BBCB200BA3E46 /* PonyDebugger */,
);
name = Network;
path = ../Classes/Network;
sourceTree = "<group>";
};
9421B8891A8BBCB200BA3E46 /* PonyDebugger */ = {
isa = PBXGroup;
children = (
9421B88A1A8BBCB200BA3E46 /* FLEXNetworkObserver.h */,
9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */,
9421B88C1A8BBCB200BA3E46 /* LICENSE */,
);
path = PonyDebugger;
sourceTree = "<group>";
};
943203FF1978F4B700E24DB3 /* 3rd Party */ = {
isa = PBXGroup;
children = (
@@ -413,6 +489,7 @@
943204001978F4C100E24DB3 /* FLEX */ = {
isa = PBXGroup;
children = (
9421B87E1A8BBCB200BA3E46 /* Network */,
944F7424197B458C009AB039 /* Object Explorers */,
944F743B197B458C009AB039 /* Utility */,
944F7444197B458C009AB039 /* Editing */,
@@ -432,8 +509,6 @@
944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */,
944F7429197B458C009AB039 /* FLEXDefaultsExplorerViewController.h */,
944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */,
944F742B197B458C009AB039 /* FLEXDescriptionTableViewCell.h */,
944F742C197B458C009AB039 /* FLEXDescriptionTableViewCell.m */,
944F742D197B458C009AB039 /* FLEXDictionaryExplorerViewController.h */,
944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */,
944F742F197B458C009AB039 /* FLEXImageExplorerViewController.h */,
@@ -448,6 +523,8 @@
944F7438197B458C009AB039 /* FLEXViewControllerExplorerViewController.m */,
944F7439197B458C009AB039 /* FLEXViewExplorerViewController.h */,
944F743A197B458C009AB039 /* FLEXViewExplorerViewController.m */,
94C681F11A3E941800E1936D /* FLEXLayerExplorerViewController.h */,
94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */,
D03647D619847235007D2A1B /* Private */,
);
name = "Object Explorers";
@@ -465,6 +542,8 @@
944F7441197B458C009AB039 /* FLEXRuntimeUtility.m */,
944F7442197B458C009AB039 /* FLEXUtility.h */,
944F7443197B458C009AB039 /* FLEXUtility.m */,
94CB48371A8EC6000054A905 /* FLEXMultilineTableViewCell.h */,
94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */,
);
name = Utility;
path = ../Classes/Utility;
@@ -496,6 +575,8 @@
children = (
944F7446197B458C009AB039 /* FLEXArgumentInputColorView.h */,
944F7447197B458C009AB039 /* FLEXArgumentInputColorView.m */,
650855E91A9007D5006109A1 /* FLEXArgumentInputDateView.h */,
650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */,
944F7448197B458C009AB039 /* FLEXArgumentInputFontView.h */,
944F7449197B458C009AB039 /* FLEXArgumentInputFontView.m */,
944F744A197B458C009AB039 /* FLEXArgumentInputJSONObjectView.h */,
@@ -544,10 +625,13 @@
944F7473197B458C009AB039 /* Global State Explorers */ = {
isa = PBXGroup;
children = (
946C6EC61A75986C006545C2 /* System Log */,
944F7474197B458C009AB039 /* FLEXClassesTableViewController.h */,
944F7475197B458C009AB039 /* FLEXClassesTableViewController.m */,
944F7476197B458C009AB039 /* FLEXFileBrowserTableViewController.h */,
944F7477197B458C009AB039 /* FLEXFileBrowserTableViewController.m */,
65F8DC6A1A8F11020076F87B /* FLEXFileBrowserFileOperationController.h */,
65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */,
0149BFE0198FAFC700B90A1B /* FLEXFileBrowserSearchOperation.h */,
0149BFE1198FAFC700B90A1B /* FLEXFileBrowserSearchOperation.m */,
944F7478197B458C009AB039 /* FLEXGlobalsTableViewController.h */,
@@ -588,6 +672,19 @@
name = "FLEX Integration";
sourceTree = "<group>";
};
946C6EC61A75986C006545C2 /* System Log */ = {
isa = PBXGroup;
children = (
946C6ECB1A759928006545C2 /* FLEXSystemLogTableViewController.h */,
946C6ECA1A759928006545C2 /* FLEXSystemLogTableViewController.m */,
946C6ECD1A7599C4006545C2 /* FLEXSystemLogMessage.h */,
946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */,
946C6EC71A7598D3006545C2 /* FLEXSystemLogTableViewCell.h */,
946C6EC81A7598D3006545C2 /* FLEXSystemLogTableViewCell.m */,
);
name = "System Log";
sourceTree = "<group>";
};
D03647D619847235007D2A1B /* Private */ = {
isa = PBXGroup;
children = (
@@ -649,6 +746,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9421B8931A8BBCB200BA3E46 /* LICENSE in Resources */,
5356825418F3656900BAAD62 /* Main_iPad.storyboard in Resources */,
53874F9918F36B1800510922 /* Localizable.strings in Resources */,
5356825918F3656900BAAD62 /* Images.xcassets in Resources */,
@@ -666,6 +764,8 @@
files = (
944F749B197B458C009AB039 /* FLEXArgumentInputNotSupportedView.m in Sources */,
535682AE18F3670300BAAD62 /* AAPLDatePickerController.m in Sources */,
946C6ECC1A759928006545C2 /* FLEXSystemLogTableViewController.m in Sources */,
946C6ECF1A7599C4006545C2 /* FLEXSystemLogMessage.m in Sources */,
535682AB18F3670300BAAD62 /* AAPLButtonViewController.m in Sources */,
944F74A2197B458C009AB039 /* FLEXArgumentInputViewFactory.m in Sources */,
944F74A1197B458C009AB039 /* FLEXArgumentInputView.m in Sources */,
@@ -674,8 +774,11 @@
944F74B1197B458C009AB039 /* FLEXInstancesTableViewController.m in Sources */,
944F7497197B458C009AB039 /* FLEXUtility.m in Sources */,
535682B018F3670300BAAD62 /* AAPLDefaultToolbarViewController.m in Sources */,
94CB48391A8EC6000054A905 /* FLEXMultilineTableViewCell.m in Sources */,
535682BD18F3670300BAAD62 /* AAPLTintedToolbarViewController.m in Sources */,
943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */,
9421B8921A8BBCB200BA3E46 /* FLEXNetworkObserver.m in Sources */,
9421B88F1A8BBCB200BA3E46 /* FLEXNetworkTransaction.m in Sources */,
944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */,
535682B318F3670300BAAD62 /* AAPLPageControlViewController.m in Sources */,
535682BA18F3670300BAAD62 /* AAPLSwitchViewController.m in Sources */,
@@ -688,14 +791,18 @@
944F74B0197B458C009AB039 /* FLEXGlobalsTableViewController.m in Sources */,
944F748B197B458C009AB039 /* FLEXDefaultsExplorerViewController.m in Sources */,
944F749E197B458C009AB039 /* FLEXArgumentInputStructView.m in Sources */,
94CB4D4F1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m in Sources */,
944F74B3197B458C009AB039 /* FLEXLiveObjectsTableViewController.m in Sources */,
944F748E197B458C009AB039 /* FLEXImageExplorerViewController.m in Sources */,
944F74B6197B458C009AB039 /* FLEXHierarchyTableViewController.m in Sources */,
65F8DC6C1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m in Sources */,
535682BC18F3670300BAAD62 /* AAPLTextViewController.m in Sources */,
9421B8901A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m in Sources */,
944F74B4197B458C009AB039 /* FLEXWebViewController.m in Sources */,
944F74A5197B458C009AB039 /* FLEXFieldEditorViewController.m in Sources */,
944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */,
535682B418F3670300BAAD62 /* AAPLPickerViewController.m in Sources */,
946C6EC91A7598D3006545C2 /* FLEXSystemLogTableViewCell.m in Sources */,
535682BE18F3670300BAAD62 /* AAPLWebViewController.m in Sources */,
944F7490197B458C009AB039 /* FLEXObjectExplorerViewController.m in Sources */,
944F7499197B458C009AB039 /* FLEXArgumentInputFontView.m in Sources */,
@@ -706,11 +813,14 @@
944F749F197B458C009AB039 /* FLEXArgumentInputSwitchView.m in Sources */,
944F74A3197B458C009AB039 /* FLEXDefaultEditorViewController.m in Sources */,
944F74AB197B458C009AB039 /* FLEXManager.m in Sources */,
94C681F31A3E941800E1936D /* FLEXLayerExplorerViewController.m in Sources */,
944F74B2197B458C009AB039 /* FLEXLibrariesTableViewController.m in Sources */,
535682BF18F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.m in Sources */,
535682B718F3670300BAAD62 /* AAPLSliderViewController.m in Sources */,
9421B88E1A8BBCB200BA3E46 /* FLEXNetworkRecorder.m in Sources */,
944F74AC197B458C009AB039 /* FLEXToolbarItem.m in Sources */,
944F74B7197B458C009AB039 /* FLEXImagePreviewViewController.m in Sources */,
650855EB1A9007D5006109A1 /* FLEXArgumentInputDateView.m in Sources */,
944F7498197B458C009AB039 /* FLEXArgumentInputColorView.m in Sources */,
944F74A0197B458C009AB039 /* FLEXArgumentInputTextView.m in Sources */,
944F749C197B458C009AB039 /* FLEXArgumentInputNumberView.m in Sources */,
@@ -718,6 +828,8 @@
944F7494197B458C009AB039 /* FLEXHeapEnumerator.m in Sources */,
535682B918F3670300BAAD62 /* AAPLStepperViewController.m in Sources */,
535682AA18F3670300BAAD62 /* AAPLAppDelegate.m in Sources */,
9421B88D1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m in Sources */,
9421B8911A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m in Sources */,
535682B518F3670300BAAD62 /* AAPLProgressViewController.m in Sources */,
535682AD18F3670300BAAD62 /* AAPLCustomToolbarViewController.m in Sources */,
944F748F197B458C009AB039 /* FLEXObjectExplorerFactory.m in Sources */,
@@ -738,7 +850,6 @@
944F7495197B458C009AB039 /* FLEXResources.m in Sources */,
535682A918F3670300BAAD62 /* AAPLAlertViewController.m in Sources */,
944F74A6197B458C009AB039 /* FLEXIvarEditorViewController.m in Sources */,
944F748C197B458C009AB039 /* FLEXDescriptionTableViewCell.m in Sources */,
0149BFE2198FAFC700B90A1B /* FLEXFileBrowserSearchOperation.m in Sources */,
535682B218F3670300BAAD62 /* AAPLMasterViewController.m in Sources */,
);
@@ -806,7 +917,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 6.0;
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -839,7 +950,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 6.0;
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
+126 -1
View File
@@ -47,5 +47,130 @@
#import "AAPLAppDelegate.h"
@implementation AAPLAppDelegate
#if DEBUG
#import "FLEXManager.h"
#endif
@interface AAPLAppDelegate () <NSURLConnectionDataDelegate, NSURLSessionDataDelegate>
@property (nonatomic, strong) NSTimer *repeatingLogExampleTimer;
@property (nonatomic, strong) NSMutableArray *connections;
@end
@implementation AAPLAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if DEBUG
[[FLEXManager sharedManager] setNetworkDebuggingEnabled:YES];
[self sendExampleNetworkRequests];
self.repeatingLogExampleTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendExampleLogMessage) userInfo:nil repeats:YES];
#endif
return YES;
}
- (void)sendExampleLogMessage
{
// To show off the system log viewer, send 20 example log messages at 1 second intervals.
static NSInteger count = 0;
NSLog(@"Example log %ld", (long)count++);
if (count > 20) {
[self.repeatingLogExampleTimer invalidate];
}
}
#pragma mark - Networking Example
- (void)sendExampleNetworkRequests
{
// Async NSURLConnection
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://api.github.com/repos/Flipboard/FLEX/issues"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
}];
// Sync NSURLConnection
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://lorempixel.com/320/480/"]] returningResponse:NULL error:NULL];
});
// NSURLSession
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 10.0;
NSURLSession *mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSMutableArray *pendingTasks = [NSMutableArray array];
// NSURLSessionDataTask with delegate
[pendingTasks addObject:[mySession dataTaskWithURL:[NSURL URLWithString:@"http://cdn.flipboard.com/serviceIcons/v2/social-icon-flipboard-96.png"]]];
// NSURLSessionDownloadTask with delegate
[pendingTasks addObject:[mySession downloadTaskWithURL:[NSURL URLWithString:@"https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png?v5"]]];
// Async NSURLSessionDownloadTask
[pendingTasks addObject:[[NSURLSession sharedSession] downloadTaskWithURL:[NSURL URLWithString:@"http://lorempixel.com/1024/1024/"] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
}]];
// Async NSURLSessionDataTask
[pendingTasks addObject:[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://api.github.com/emojis"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}]];
// Async NSURLSessionUploadTask
NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://google.com/"]];
uploadRequest.HTTPMethod = @"POST";
NSData *data = [@"q=test" dataUsingEncoding:NSUTF8StringEncoding];
[pendingTasks addObject:[mySession uploadTaskWithRequest:uploadRequest fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}]];
// Remaining requests made through NSURLConnection with a delegate
NSArray *requestURLStrings = @[ @"http://lorempixel.com/400/400/",
@"http://google.com",
@"http://search.cocoapods.org/api/pods?query=FLEX&amount=1",
@"https://api.github.com/users/Flipboard/repos",
@"http://info.cern.ch/hypertext/WWW/TheProject.html",
@"https://api.github.com/repos/Flipboard/FLEX/issues",
@"https://cloud.githubusercontent.com/assets/516562/3971767/e4e21f58-27d6-11e4-9b07-4d1fe82b80ca.png",
@"http://hipsterjesus.com/api?paras=1&type=hipster-centric&html=false",
@"http://lorempixel.com/750/1334/" ];
NSTimeInterval delayTime = 10.0;
const NSTimeInterval stagger = 1.0;
// Send off the NSURLSessionTasks (staggered)
for (NSURLSessionTask *task in pendingTasks) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[task resume];
});
delayTime += stagger;
}
// Begin the NSURLConnection requests (staggered)
self.connections = [NSMutableArray array];
for (NSString *urlString in requestURLStrings) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[self.connections addObject:[[NSURLConnection alloc] initWithRequest:request delegate:self]];
});
delayTime += stagger;
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
completionHandler(NSURLSessionResponseAllow);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self.connections removeObject:connection];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self.connections removeObject:connection];
}
@end
@@ -11,10 +11,6 @@
#import "FLEXManager.h"
#endif
@interface AAPLCatalogTableTableViewController ()
@end
@implementation AAPLCatalogTableTableViewController
- (void)viewDidLoad
@@ -22,6 +18,7 @@
[super viewDidLoad];
#if DEBUG
[self registerViewControllerBasedOption];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"FLEX" style:UIBarButtonItemStylePlain target:self action:@selector(flexButtonTapped:)];
#endif
}
@@ -34,4 +31,39 @@
#endif
}
- (void)registerViewControllerBasedOption
{
// create UIViewController subclass
UIViewController *viewController = [[UIViewController alloc] init];
// fill it with some stuff
UILabel *infoLabel = [[UILabel alloc] init];
infoLabel.translatesAutoresizingMaskIntoConstraints = NO;
infoLabel.text = @"Add switches, notes or whatewer you wish to provide your testers with superpowers!";
infoLabel.numberOfLines = 0;
infoLabel.textAlignment = NSTextAlignmentCenter;
UIView *view = viewController.view;
view.backgroundColor = [UIColor whiteColor];
[view addSubview:infoLabel];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[infoLabel]-0-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(infoLabel)]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[infoLabel]-0-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(infoLabel)]];
// return it in viewControllerFutureBlock
[[FLEXManager sharedManager] registerGlobalEntryWithName:@"🛃 Custom Superpowers"
viewControllerFutureBlock:^id{
return viewController;
}];
}
@end
@@ -74,7 +74,10 @@
// "More" bar button item is correctly transferred to the destination detail view controller's navigation item. We are only concerned about
// this change when the application is in portrait mode since this is the only time that the "More" bar button item will be visible.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
#pragma clang diagnostic pop
UINavigationController *newDetailViewController = [segue destinationViewController];
UINavigationController *oldDetailViewController = [self.splitViewController.viewControllers lastObject];
@@ -1,46 +1,69 @@
{
"images" : [
{
"orientation" : "portrait",
"idiom" : "iphone",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"idiom" : "iphone",
"subtype" : "736h",
"filename" : "iPhone6PlusPortrait.png",
"minimum-system-version" : "8.0",
"orientation" : "portrait",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "736h",
"filename" : "iPhone6PlusLandscape.png",
"minimum-system-version" : "8.0",
"orientation" : "landscape",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "667h",
"filename" : "iPhone6Portrait.png",
"minimum-system-version" : "8.0",
"orientation" : "portrait",
"scale" : "2x"
},
{
"orientation" : "portrait",
"idiom" : "iphone",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"filename" : "Launch@2x.png",
"scale" : "2x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "retina4",
"extent" : "full-screen",
"filename" : "Launch@2x~568h.png",
"minimum-system-version" : "7.0",
"orientation" : "portrait",
"scale" : "2x"
},
{
"orientation" : "portrait",
"idiom" : "ipad",
"idiom" : "iphone",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"scale" : "1x"
},
{
"orientation" : "landscape",
"idiom" : "ipad",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"filename" : "Launch.png",
"scale" : "1x"
},
{
"orientation" : "portrait",
"idiom" : "ipad",
"idiom" : "iphone",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"filename" : "Launch@2x.png",
"scale" : "2x"
},
{
"orientation" : "landscape",
"idiom" : "ipad",
"orientation" : "portrait",
"idiom" : "iphone",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"filename" : "Launch@2x~568h.png",
"subtype" : "retina4",
"scale" : "2x"
}
],
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+5 -2
View File
@@ -1,13 +1,15 @@
Pod::Spec.new do |spec|
spec.name = "FLEX"
spec.version = "1.1.0"
spec.version = "2.0.1"
spec.summary = "A set of in-app debugging and exploration tools for iOS"
spec.description = <<-DESC
- Inspect and modify views in the hierarchy.
- View Detailed network request history.
- See the properties and ivars on any object.
- Dynamically modify many properties and ivars.
- Dynamically call instance and class methods.
- Access any live object via a scan of the heap.
- See system log messages (i.e. from `NSLog()`).
- View the file system within your app's sandbox.
- Explore all classes in your app and linked systems frameworks (public and private).
- Quickly access useful objects such as `[UIApplication sharedApplication]`, the app delegate, the root view controller on the key window, and more.
@@ -27,9 +29,10 @@ Pod::Spec.new do |spec|
spec.license = { :type => "BSD", :file => "LICENSE" }
spec.author = { "Ryan Olson" => "ryanolsonk@gmail.com" }
spec.social_media_url = "https://twitter.com/ryanolsonk"
spec.platform = :ios, "6.0"
spec.platform = :ios, "7.0"
spec.source = { :git => "https://github.com/Flipboard/FLEX.git", :tag => "#{spec.version}" }
spec.source_files = "Classes/**/*.{h,m}"
spec.frameworks = "CoreGraphics"
spec.libraries = "z"
spec.requires_arc = true
end
+9 -16
View File
@@ -9,6 +9,8 @@ FLEX (Flipboard Explorer) is a set of in-app debugging and exploration tools for
- See the properties and ivars on any object.
- Dynamically modify many properties and ivars.
- Dynamically call instance and class methods.
- Observe detailed network request history with timing, headers, and full responses.
- View system log messages (e.g. from `NSLog`).
- Access any live object via a scan of the heap.
- View the file system within your app's sandbox.
- Explore all classes in your app and linked systems frameworks (public and private).
@@ -52,6 +54,11 @@ Once a view is selected, you can tap on the info bar below the toolbar to presen
![View Modification](http://engineering.flipboard.com/assets/flex/advanced-view-editing.gif)
### Network History
When enabled, network debugging allows you to view all requests made using NSURLConnection or NSURLSession. Settings allow you to adjust what kind of response bodies get cached and the maximum size limit of the response cache. You can choose to have network debugging enabled automatically on app launch. This setting is persisted accross launches.
![Network History](http://engineering.flipboard.com/assets/flex/network-history.gif)
### All Objects on the Heap
FLEX queries malloc for all the live allocated memory blocks and searches for ones that look like objects. You can see everything from here.
@@ -117,30 +124,16 @@ FLEX builds on ideas and inspiration from open source tools that came before it.
- [heap_find.cpp](https://www.opensource.apple.com/source/lldb/lldb-179.1/examples/darwin/heap_find/heap/heap_find.cpp): an example of enumerating malloc blocks for finding objects on the heap.
- [Gist](https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396) from [@samdmarshall](https://github.com/samdmarshall): another example of enumerating malloc blocks.
- [Non-pointer isa](http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html): an explanation of changes to the isa field on iOS for ARM64 and mention of the useful `objc_debug_isa_class_mask` variable.
- [GZIP](https://github.com/nicklockwood/GZIP): A library for compressing/decompressing data on iOS using libz.
## Contributing
We welcome pull requests for bug fixes, new features, and improvements to FLEX. Contributors to the main FLEX repository must accept Flipboard's Apache-style [Individual Contributor License Agreement (CLA)](https://docs.google.com/forms/d/1gh9y6_i8xFn6pA15PqFeye19VqasuI9-bGp_e0owy74/viewform) before any changes can be merged.
## Changes
#### v1.2.0 (in progress)
- Search bar filtering and sorting by file size in the file browser (@DaidoujiChen)
#### v1.1.0 2014/7/31
- Support adding custom entries to the "Globals" menu (@JaviSoto)
- Warnings cleanup (@JaviSoto)
- WYSIWYG font picker input view (@DaidoujiChen)
- Small bug fixes around status bar styling, rotation, and ivar value display (@ryanolsonk)
#### v1.0.1 2014/07/26
- Warnings cleanup and small bugfixes with rotation and interface orientations (@ryanolsonk)
## TODO
- Swift runtime introspection (swift classes, swift objects on the heap, etc.)
- Network request logging
- Improved file type detection and display in the file browser
- Add new NSUserDefault key/value pairs on the fly