motion-xray 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +21 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +28 -0
  4. data/README.md +426 -0
  5. data/Rakefile +11 -0
  6. data/app/app_delegate.rb +44 -0
  7. data/lib/motion-xray.rb +37 -0
  8. data/lib/motion-xray/plugins/accessibility_plugin.rb +129 -0
  9. data/lib/motion-xray/plugins/log_plugin.rb +301 -0
  10. data/lib/motion-xray/plugins/save_ui_plugin.rb +142 -0
  11. data/lib/motion-xray/plugins/ui_plugin.rb +41 -0
  12. data/lib/motion-xray/version.rb +5 -0
  13. data/lib/motion-xray/views/xray_color_swatch.rb +234 -0
  14. data/lib/motion-xray/views/xray_dpad.rb +142 -0
  15. data/lib/motion-xray/views/xray_gradient_view.rb +23 -0
  16. data/lib/motion-xray/views/xray_headers.rb +101 -0
  17. data/lib/motion-xray/views/xray_lock_button.rb +50 -0
  18. data/lib/motion-xray/views/xray_scroll_view.rb +12 -0
  19. data/lib/motion-xray/views/xray_toolbar.rb +173 -0
  20. data/lib/motion-xray/views/xray_window.rb +13 -0
  21. data/lib/motion-xray/xray.rb +56 -0
  22. data/lib/motion-xray/xray_constants.rb +5 -0
  23. data/lib/motion-xray/xray_dummy.rb +8 -0
  24. data/lib/motion-xray/xray_editors.rb +62 -0
  25. data/lib/motion-xray/xray_ext.rb +125 -0
  26. data/lib/motion-xray/xray_plugin.rb +40 -0
  27. data/lib/motion-xray/xray_typewriter.rb +217 -0
  28. data/lib/motion-xray/xray_ui.rb +723 -0
  29. data/lib/motion-xray/z_editors/xray_boolean_editor.rb +24 -0
  30. data/lib/motion-xray/z_editors/xray_color_editor.rb +119 -0
  31. data/lib/motion-xray/z_editors/xray_frame_editor.rb +108 -0
  32. data/lib/motion-xray/z_editors/xray_image_editor.rb +78 -0
  33. data/lib/motion-xray/z_editors/xray_text_editor.rb +94 -0
  34. data/lib/resources/xray_button_bg@2x.png +0 -0
  35. data/lib/resources/xray_choose_button@2x.png +0 -0
  36. data/lib/resources/xray_clear_button@2x.png +0 -0
  37. data/lib/resources/xray_detail_button@2x.png +0 -0
  38. data/lib/resources/xray_dpad@2x.png +0 -0
  39. data/lib/resources/xray_dpad_center@2x.png +0 -0
  40. data/lib/resources/xray_dpad_down@2x.png +0 -0
  41. data/lib/resources/xray_dpad_left@2x.png +0 -0
  42. data/lib/resources/xray_dpad_right@2x.png +0 -0
  43. data/lib/resources/xray_dpad_up@2x.png +0 -0
  44. data/lib/resources/xray_drawer_left@2x.png +0 -0
  45. data/lib/resources/xray_drawer_right@2x.png +0 -0
  46. data/lib/resources/xray_edit_button@2x.png +0 -0
  47. data/lib/resources/xray_email_button@2x.png +0 -0
  48. data/lib/resources/xray_lock_button_horizontal@2x.png +0 -0
  49. data/lib/resources/xray_lock_button_locked@2x.png +0 -0
  50. data/lib/resources/xray_lock_button_unlocked@2x.png +0 -0
  51. data/lib/resources/xray_lock_button_vertical@2x.png +0 -0
  52. data/motion-xray.gemspec +40 -0
  53. data/resources/Default-568h@2x.png +0 -0
  54. data/spec/xray_view_spec.rb +43 -0
  55. data/vendor/Podfile.lock +11 -0
  56. metadata +177 -0
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ .repl_history
2
+ build
3
+ tags
4
+ app/pixate_code.rb
5
+ resources/*.nib
6
+ resources/*.momd
7
+ resources/*.storyboardc
8
+ .DS_Store
9
+ nbproject
10
+ .redcar
11
+ #*#
12
+ *~
13
+ *.sw[po]
14
+ .eprj
15
+ .sass-cache
16
+ .idea
17
+ *.gem
18
+ *.pxm
19
+ /vendor/Pods/
20
+ /vendor/Pods
21
+ .dat*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ motion-xray (1.0.3)
5
+ geomotion (>= 0.10.0)
6
+ sugarcube (>= 0.20.1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ diff-lcs (1.1.3)
12
+ geomotion (0.10.0)
13
+ rspec (2.12.0)
14
+ rspec-core (~> 2.12.0)
15
+ rspec-expectations (~> 2.12.0)
16
+ rspec-mocks (~> 2.12.0)
17
+ rspec-core (2.12.2)
18
+ rspec-expectations (2.12.1)
19
+ diff-lcs (~> 1.1.3)
20
+ rspec-mocks (2.12.2)
21
+ sugarcube (0.20.1)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ motion-xray!
28
+ rspec
data/README.md ADDED
@@ -0,0 +1,426 @@
1
+ Motion-Xray
2
+ ====
3
+
4
+ Developer tools for iOS. Runs on the device, no browser or computer needed.
5
+
6
+ (think Firebug or Webkit Developer Tools)
7
+
8
+ TL;DR
9
+ -----
10
+
11
+ 1. `gem install motion-xray`
12
+ 2. Replace `UIWindow` with `Motion::Xray::XrayWindow`
13
+
14
+ The Problem
15
+ -----------
16
+
17
+ During development we rely heavily on the simulator to quickly view and test
18
+ features, but often when we finally install our app on a device, the experience
19
+ is not up-to-snuff with what was going on in the simulator. Views are off by a
20
+ few pixels, performance is not what we expect, and crashes occur where we never
21
+ saw them in the simulator. Sometimes these are device problems, sometimes it
22
+ has to do with dropping in and out of signal, all sorts of scenarios that we
23
+ cannot easily test for in the simulator.
24
+
25
+ And of course there is the problem that iOS devices have more features than the
26
+ simulator! Bluetooth 4, for example, is not easy to get setup in the simulator
27
+ (and you have to buy a USB bluetooth module).
28
+
29
+ My thesis is that we need to make on-device testing a more enjoyable and useful
30
+ testing environment, so that we are compelled to test on it sooner and more
31
+ often.
32
+
33
+ My Proposal
34
+ -----------
35
+
36
+ Motion-Xray is such a solution. During development you can use Xray as a UI
37
+ inspector, or to monitor the console log, preview how accessibile your app is
38
+ (to blind and color blind developers), or you can create a plugin that provides
39
+ information specifically useful to your app. Below I'll show how to create a
40
+ new plugin. Check out the [plugins folder][] for some examples.
41
+
42
+ Features
43
+ -----
44
+
45
+ If you clone and run Xray in the simulator, you will see a very boring app:
46
+
47
+ ![Xray Screenshot](http://media.colinta.com/xray/xray_app.png)
48
+
49
+ Activate a "shake" gesture by pressing ⌘⌃Z and Xray will activate, which
50
+ displays this:
51
+
52
+ ![Xray Screenshot](http://media.colinta.com/xray/xray.png)
53
+
54
+ The application shrinks down to a quarter size, and the development environment
55
+ takes up the remaining space. That is Xray, an in-app debugging and development
56
+ environment! :-D
57
+
58
+ Features
59
+ ------------
60
+
61
+ That's enough to have the `Motion::Xray.toggle` command fired whenever you shake
62
+ the device. If you want to use some other mechanism that launches Xray (a
63
+ complicated gesture recognizer would be a good candidate), you can call
64
+ `Xray.toggle` (which calls either `Xray.fire_up` or `Xray.cool_down`). The
65
+ `Motion::Xray::XrayWindow` class is only used to listen for the shake event, so
66
+ using it will not affect your app in any other way.
67
+
68
+ When you shake your phone and activate Xray, you are presented with three panes
69
+ and a toolbar at the bottom:
70
+
71
+ ![Preview panes](http://media.colinta.com/xray/xray_panes.png)
72
+
73
+ ### 1. Preview
74
+
75
+ All the views under the main window are placed in the `Preview` area:
76
+
77
+ ![Preview pane](http://media.colinta.com/xray/xray_preview.png)
78
+
79
+ If you touch this area, you can get a quick preview of the view, or you can
80
+ quickly change to another view, or change orientation. After a few seconds,
81
+ Xray will automatically be displayed again. If you want to leave the Xray debug
82
+ area, you should shake the phone again.
83
+
84
+ ### 2. UI Selector
85
+
86
+ This pane shows the view hierarchy of your app:
87
+
88
+ ![UI selector pane](http://media.colinta.com/xray/xray_uiselector.png)
89
+
90
+ All the views on screen can be selected here, and a red box will show the bounds
91
+ of that view in the `Preview` pane. If you touch it again, that view will be
92
+ sent to whatever plugin you have visible, or you can press the "down" button in
93
+ the bottom-right corner of this pane.
94
+
95
+ Not all plugins respond to the selected view. For instance the accessibility
96
+ plugin will always display the entire screen, regardless of which view is
97
+ selected. The log plugin, on the other hand, displays the `inspect` information
98
+ about the view. And of course the UI plugin will change so that you can edit
99
+ the properties of that view.
100
+
101
+ The button in the upper-left corner expands this view, so that you can see the
102
+ tree easier.
103
+
104
+ ![Expanded tree view](http://media.colinta.com/xray/xray_tree.png)
105
+
106
+ In the upper-right corner is the button to activate a visual view selector:
107
+
108
+ ![UIView selector](http://media.colinta.com/xray/xray_uiselector.png)
109
+
110
+ You can tap a view to get information about it, or press and hold to make that
111
+ view "go away" so that you can choose the view *behind* it, or double-tap to
112
+ select that view.
113
+
114
+ ### 3. Plugin Canvas
115
+
116
+ Here's where the inspector and other plugins live, with a toolbar at the bottom
117
+ to select what plugin you want to view:
118
+
119
+ ![Plugin pane](http://media.colinta.com/xray/xray_canvas.png)
120
+
121
+ It is very easy to create new plugins, I'll go over that below. After you
122
+ create a new plugin, you register it with Xray:
123
+
124
+ ```ruby
125
+ Xray.register(YourPlugin.new)
126
+ ```
127
+
128
+ Built-in plugins
129
+ ----------------
130
+
131
+ ### UI (`Motion::Xray::UIPlugin`)
132
+
133
+ **included automatically**
134
+
135
+ The original idea for Xray was just this UI plugin. The other plugins came
136
+ later. I realized that it could (and should) be a generic "development
137
+ environment" instead of a "UI editor". Also, some early feedback from the
138
+ HipByte team helped open up this world of possibilities. :-)
139
+
140
+ `UIPlugin` uses a pluggable architecture. First, there are the editors:
141
+
142
+ - `Motion::Xray::TextEditor`
143
+ - `Motion::Xray::ColorEditor`
144
+ - `Motion::Xray::BooleanEditor`
145
+ - `Motion::Xray::FrameEditor`
146
+
147
+ Second, these editors get associated with the view properties in a `Hash` that
148
+ is returned by the class method `UIView##xray`. In custom views you only need
149
+ to return the properties that *your custom view uses*; any editable properties
150
+ in views you inherit from will be included. Don't do any merging in your `xray`
151
+ method, that is handled by the plugin (by `UIView##build_xray`, in
152
+ `xray_ext.rb`)
153
+
154
+ ```ruby
155
+ class << UILabel
156
+ def xray
157
+ {
158
+ 'Content': { # section name
159
+ text: Motion::Xray::TextEditor, # property => editor class
160
+ }
161
+ }
162
+ end
163
+ end
164
+ ```
165
+
166
+ If you inherit from a view and you want to *disable* one of the editors, assign
167
+ `nil` as the editor for that property. `UIWindow` does this to prevent editing
168
+ `frame`, `hidden`, and `userInteractionEnabled` properties from getting changed.
169
+
170
+ ```ruby
171
+ class << UIWindow
172
+ def xray
173
+ {
174
+ 'TurnOff' => {
175
+ frame: nil,
176
+ hidden: nil,
177
+ userInteractionEnabled: nil,
178
+ },
179
+ }
180
+ end
181
+ end
182
+ ```
183
+
184
+ Writing custom editors can be time consuming, because they are often very UI
185
+ heavy (check out the `ColorEditor` to see what I mean). That said, the concept
186
+ is very easy:
187
+
188
+ 1. extend the `Motion::Xray::PropertyEditor` class.
189
+ 2. Return your editor in the `edit_view(container_width)` method. You don't have
190
+ to use the entire width, but your editor view can't be any wider.
191
+
192
+ If you want, you can return a "preview" that just shows the value, with a
193
+ button that opens a much larger editor. `ColorEditor` and `TextEditor`
194
+ behave this way.
195
+ 3. To get the value of the property being edited, use the method `get_value`. It
196
+ will introspect `self.target` looking for a the appropriate getter method.
197
+ 4. Whenever the value changes, assign the new value to `set_value`, and that
198
+ will fire a `XrayTargetDidChangeNotification` notification, which is used by
199
+ `Motion::Xray::SaveUIPlugin`. `set_value` will, like `get_value`, look for an
200
+ appropriate setter.
201
+
202
+ The editors should be able to be used for many properties, but if you're writing
203
+ a one-off editor, I suppose you could call the getters and setters directly, but
204
+ you should post the `XrayTargetDidChangeNotification` notification if you do
205
+ this.
206
+
207
+ ### Save UI (`Motion::Xray::SaveUIPlugin`)
208
+
209
+ After you have made your changes to your `UIView`s, you will want to save those
210
+ changes, right? This plugin is your friend. It is not included by default,
211
+ though, because not everyone uses [teacup][] or [pixate][].
212
+
213
+ Many of the properties that you'll be editing will already have the appropriate
214
+ output in this plugin (it uses `#inspect`), but the way that Xray records your
215
+ changes can be customized in two ways:
216
+
217
+ 1. Change the `type` of output that you want. The default is `teacup`, but it
218
+ is possible to setup the `SaveUIPlugin` to record NUI or Pixate changes as
219
+ well.
220
+
221
+ ```ruby
222
+ Motion::Xray.registerPlugin(Motion::Xray::SaveUIPlugin.new) # use teacup
223
+ Motion::Xray.registerPlugin(Motion::Xray::SaveUIPlugin.new(:pixate))
224
+ ```
225
+ 2. Register custom output, by class. This will be used for any property, for
226
+ instance if you want `UIColor` objects to be persisted as an array of RGB
227
+ values, you could register that output like this:
228
+
229
+ ```ruby
230
+ register(:teacup, UIColor) { |color| "[#{color.red}, #{color.blue}, #{color.green}]" }
231
+ ```
232
+
233
+ Because Xray uses SugarCube, a lot of the hard work is done for us there
234
+ (because SugarCube implements lots of useful `to_s` and `inspect` methods)
235
+
236
+ ### Accessibility (`Motion::Xray::AccessibilityPlugin`)
237
+
238
+ **included automatically**
239
+
240
+ This plugin provides two screenshots of the current screen. One that mimics how
241
+ a sightless person would "see" your app, and another that mimics how a (very)
242
+ color blind person would see it. Each one is *at best*, an *approximation*, but
243
+ the goal is that having this quick metric handy will encourage more developers
244
+ to spend some time on accessibility. A little goes a long way!
245
+
246
+ ![Accessibility Plugin](http://media.colinta.com/xray/xray_accessibility.png)
247
+
248
+ This plugin generated a lot of excitement when I announced Xray at the
249
+ RubyMotion conference, #inspect2013. We had all heard [Austin
250
+ Seraphin's][austinseraphin] talk the previous day, about how to improve
251
+ accessibility. This plugin tries to provide a visualization of the
252
+ recommendations he gave us - first and foremost, he recommended that you should
253
+ at least set the `accessibilityLabel` on custom views.
254
+
255
+ The left side shows a screenshot of your app with only red and green squares.
256
+ Green squares mean "you're doing OK". It does NOT mean that your app has "good"
257
+ accessibility, but at a minimum you should at least get all your screens "in the
258
+ green" before you send your app to an accessibility consultant.
259
+
260
+ The other screenshot is a your app in black and white, with colors desaturated.
261
+ An attempt to mimic how a color blind person would see your app. There are
262
+ *many* types of color blindness, and down the road I would love to see a few
263
+ different screen shots for each specific type in this pane. For now, it takes
264
+ the "common denominator" approach, which is to remove *all* color.
265
+
266
+ ### Log (`Motion::Xray::LogPlugin`)
267
+
268
+ **included automatically**
269
+
270
+ ![Log Plugin](http://media.colinta.com/xray/xray_log.png)
271
+
272
+ This plugin requires more involvement in your application code, if you want to
273
+ make it useful. You basically need to use the `Motion::Xray::Log.log` family of methods,
274
+ and each of them will write to the `Motion::Xray::LogPlugin.log` buffer. Here's a quick
275
+ way to do this:
276
+
277
+ ```ruby
278
+ Log = Motion::Xray::Log
279
+
280
+ Log.info('info!')
281
+ Log.error('an error occurred!')
282
+ # available methods:
283
+ # Log.error, Log.warning, Log.log, Log.notice, Log.info, Log.ok, Log.debug
284
+
285
+ # only log information greater than or equal to log level "warning"
286
+ Log.level = Log::Warning
287
+ ```
288
+
289
+ Or you can write a log method yourself that calls one of the Motion::Xray::Log methods.
290
+ If you use CocoaLumberjack, it should be very easy to hook up `Motion::Xray::Log`, but
291
+ it will have to be done in Obj-C I think (I took a stab at it, but gave up when
292
+ I couldn't access the `message` property).
293
+
294
+ The upside to using these `Motion::Xray::Log` methods is that they use pretty coloring,
295
+ they output to both the console *and* the Xray log, and I'm planning on
296
+ including some [awesome-print][]-like features to the log methods in the future
297
+ (or, more likely, delegate to awesome-print if it's available).
298
+
299
+ Writing an Xray plugin
300
+ ----------------
301
+
302
+ My hope is that you will identify places in your app where you would benefit
303
+ from on-device feedback. Here are just some ideas as examples:
304
+
305
+ 1. **Building an app that interacts with bluetooth devices:** How about
306
+ signal strength? Devices detected? Connect and disconnect buttons?
307
+ 2. **Interfacing with an API:** Logging requests, logging parameters sent and
308
+ responses, interface to send arbitrary requests
309
+ 3. **Building a game:** framerate, number of textures on screen. To find out
310
+ when the performance breaks down on the device, you can't trust the
311
+ simulator!
312
+
313
+ So, let's get to it. I will use some code from `AccessibilityPlugin` in this
314
+ example.
315
+
316
+ First, the most basic plugin structure:
317
+
318
+ ```ruby
319
+ class AccessibilityPlugin < Plugin
320
+ name 'Accessibility' # as you want it to appear in the toolbar
321
+
322
+ # canvas is the view where the plugin will be placed. You do not need to
323
+ # call `addSubview` on this object.
324
+ def plugin_view(canvas)
325
+ return UIView.initWithFrame(canvas.bounds)
326
+ end
327
+
328
+ end
329
+ ```
330
+
331
+ So far we have:
332
+
333
+ - named our plugin 'Accessibility'
334
+ - returned an empty container
335
+
336
+ Let's add our two image views. We'll make use of geomotion, which is required
337
+ by Xray:
338
+
339
+ ```ruby
340
+ def plugin_view(canvas)
341
+ return UIView.alloc.initWithFrame(canvas.bounds).tap do |view|
342
+ view.backgroundColor = :black.uicolor
343
+
344
+ @accessibility = UIButton.alloc.initWithFrame(view.bounds
345
+ .thinner(view.bounds.width / 2))
346
+ @colorblind = UIButton.alloc.initWithFrame(view.bounds
347
+ .thinner(view.bounds.width / 2)
348
+ .right(view.bounds.width / 2))
349
+
350
+ view << @accessibility
351
+ view << @colorblind
352
+ end
353
+ end
354
+ ```
355
+
356
+ When the plugin is activated, we should grab a screenshot of the app and assign
357
+ it to each view. The `show` method is called on a plugin when it is selected.
358
+
359
+ ```ruby
360
+ def show
361
+ Dispatch::Queue.main.async do
362
+ @colorblind.setImage(get_colorblind_image, forState: :normal.uicontrolstate)
363
+ end
364
+ Dispatch::Queue.main.async do
365
+ @accessibility.setImage(get_accessibility_image, forState: :normal.uicontrolstate)
366
+ end
367
+ end
368
+ ```
369
+
370
+ The `AccessibilityPlugin` does a few more things like show spinners, display a
371
+ big screenshot image on touch, and I haven't implemented the
372
+ `get_{accessibility,colorblind}_image` methods here, but hopefully this is
373
+ enough for you to get the gist of writing a plugin. Here is the entire list of
374
+ methods that you can call, or get called, on a plugin:
375
+
376
+ #### Properties
377
+
378
+ - `name` - the name as it appears in the toolbar
379
+ - `view` - stores the plugin view that is returned by `plugin_view`. This
380
+ method is only created *once* (much like `UIViewController#loadView`)
381
+ - `target` - the view that has been selected in the UI picker
382
+
383
+ #### Methods you must implement
384
+
385
+ - `plugin_view(canvas_view)` - the view returned by this method will be placed
386
+ in `canvas_view` when your plugin is selected
387
+ - `edit(target)` - called when a new view is double-tapped in the UI picker.
388
+ You should call `super`, which assigns this view to the `target` property.
389
+ Then you can update `self.view` with any changes that you need to apply.
390
+ - `show` - called when your plugin is selected (this will always be after
391
+ `edit(target)`)
392
+ - `hide` - called just before your plugin is removed from the canvas
393
+
394
+ #### Registering your plugin
395
+
396
+ Register your new plugin in the
397
+ `AppDelegate#application(didFinishLaunchingWithOptions:)` method.
398
+
399
+ ```ruby
400
+ class AppDelegate
401
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
402
+ @window = Motion::Xray::XrayWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
403
+ @window.makeKeyAndVisible
404
+
405
+ # include the SaveUIPlugin, which is not included by default
406
+ Motion::Xray.register(Motion::Xray::SaveUIPlugin.new)
407
+
408
+ # include a custom plugin
409
+ Motion::Xray.register(CustomPlugin.new)
410
+
411
+ return true
412
+ end
413
+ end
414
+ ```
415
+
416
+ Dependencies
417
+ ------------
418
+
419
+ Xray depends on geomotion, which I don't feel bad about, and SugarCube. I would
420
+ consider removing the SugarCube dependency, because not everyone uses it, but
421
+ SugarCube adds a ton of benefit (like `#to_s` and `UIColor` additions).
422
+
423
+ [plugins folder]: https://github.com/colinta/motion-xray/tree/master
424
+ [awesome-print]: https://github.com/michaeldv/awesome_print_motion
425
+ [teacup]: https://github.com/rubymotion/teacup
426
+ [pixate]: http://www.pixate.com