motion-xray 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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