motion-wiretap 0.1.0
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.
- data/README.md +16 -0
- data/lib/motion-wiretap/all/signal.rb +22 -0
- data/lib/motion-wiretap/all/wiretap.rb +447 -0
- data/lib/motion-wiretap/all/wiretap_exts.rb +30 -0
- data/lib/motion-wiretap/ios/wiretap_control_events.rb +84 -0
- data/lib/motion-wiretap/ios/wiretap_exts_ios.rb +24 -0
- data/lib/motion-wiretap/ios/wiretap_gestures.rb +143 -0
- data/lib/motion-wiretap/ios/wiretap_ios.rb +55 -0
- data/lib/motion-wiretap/osx/wiretap_exts_osx.rb +7 -0
- data/lib/motion-wiretap/osx/wiretap_osx.rb +6 -0
- data/lib/motion-wiretap/version.rb +3 -0
- data/motion-wiretap.gemspec +30 -0
- metadata +76 -0
data/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
MotionWiretap
|
2
|
+
-------
|
3
|
+
|
4
|
+
An iOS / OS X wrapper heavily inspired by ReactiveCocoa.
|
5
|
+
|
6
|
+
gem coming soon, just need to write the gemspec
|
7
|
+
|
8
|
+
run the OSX specs using `rake spec platform=osx`
|
9
|
+
|
10
|
+
Showdown
|
11
|
+
-------
|
12
|
+
|
13
|
+
Open these side by side to see the comparison:
|
14
|
+
|
15
|
+
[reactive.rb](https://gist.github.com/colinta/d0a273f8d858a8f61c73)
|
16
|
+
[reactive.mm](https://gist.github.com/colinta/5cfa588fed7b929193ae)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Wiretap
|
2
|
+
|
3
|
+
# a Wiretap::Signal is much like a Promise in functional programming. A
|
4
|
+
# Signal is triggered with a new value, or it is completed, or canceled with
|
5
|
+
# an error event.
|
6
|
+
class Signal
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
end
|
10
|
+
|
11
|
+
def next(value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def complete
|
15
|
+
end
|
16
|
+
|
17
|
+
def error
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,447 @@
|
|
1
|
+
$motion_wiretaps = []
|
2
|
+
|
3
|
+
module MotionWiretap
|
4
|
+
|
5
|
+
class Wiretap
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
$motion_wiretaps << self # signal will be removed when it is completed
|
9
|
+
|
10
|
+
@is_torn_down = false
|
11
|
+
@is_completed = false
|
12
|
+
@is_error = false
|
13
|
+
@queue = nil
|
14
|
+
|
15
|
+
@listener_handlers = []
|
16
|
+
@completion_handlers = []
|
17
|
+
@error_handlers = []
|
18
|
+
|
19
|
+
listen &block if block
|
20
|
+
end
|
21
|
+
|
22
|
+
def dealloc
|
23
|
+
teardown
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
return if @is_torn_down
|
29
|
+
|
30
|
+
@is_torn_down = true
|
31
|
+
$motion_wiretaps.remove(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
# specify the GCD queue that the listeners should be run on
|
35
|
+
def queue(queue)
|
36
|
+
@queue = queue
|
37
|
+
return self
|
38
|
+
end
|
39
|
+
|
40
|
+
# send a block to the GCD queue
|
41
|
+
def enqueue(&block)
|
42
|
+
if @queue
|
43
|
+
@queue.async(&block)
|
44
|
+
else
|
45
|
+
block.call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
##|
|
50
|
+
##| Wiretap events
|
51
|
+
##|
|
52
|
+
|
53
|
+
# called when the value changes
|
54
|
+
def listen(wiretap=nil, &block)
|
55
|
+
raise "Block or Wiretap is expected in #{self.class.name}##{__method__}" unless block || wiretap
|
56
|
+
raise "Only Block *or* Wiretap is expected in #{self.class.name}##{__method__}" if block && wiretap
|
57
|
+
@listener_handlers << (block || wiretap)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def trigger_changed(*values)
|
62
|
+
return if @is_torn_down || @is_completed || @is_error
|
63
|
+
|
64
|
+
@listener_handlers.each do |block_or_wiretap|
|
65
|
+
trigger_changed_on(block_or_wiretap, values)
|
66
|
+
end
|
67
|
+
|
68
|
+
return self
|
69
|
+
end
|
70
|
+
|
71
|
+
def trigger_changed_on(block_or_wiretap, values)
|
72
|
+
if block_or_wiretap.is_a? Wiretap
|
73
|
+
block_or_wiretap.trigger_changed(*values)
|
74
|
+
else
|
75
|
+
enqueue do
|
76
|
+
block_or_wiretap.call(*values)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# called when no more values are expected
|
82
|
+
def and_then(wiretap=nil, &block)
|
83
|
+
raise "Block or Wiretap is expected in #{self.class.name}##{__method__}" unless block || wiretap
|
84
|
+
raise "Only Block *or* Wiretap is expected in #{self.class.name}##{__method__}" if block && wiretap
|
85
|
+
@completion_handlers << (block || wiretap)
|
86
|
+
if @is_completed
|
87
|
+
trigger_completed_on(block || wiretap)
|
88
|
+
end
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def trigger_completed
|
93
|
+
return if @is_torn_down || @is_completed || @is_error
|
94
|
+
@is_completed = true
|
95
|
+
|
96
|
+
@completion_handlers.each do |block_or_wiretap|
|
97
|
+
trigger_completed_on(block_or_wiretap)
|
98
|
+
end
|
99
|
+
|
100
|
+
teardown
|
101
|
+
return self
|
102
|
+
end
|
103
|
+
|
104
|
+
def trigger_completed_on(block_or_wiretap)
|
105
|
+
if block_or_wiretap.is_a? Wiretap
|
106
|
+
block_or_wiretap.trigger_completed
|
107
|
+
else
|
108
|
+
enqueue do
|
109
|
+
block_or_wiretap.call
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# called when an error occurs, and no more values are expected
|
115
|
+
def on_error(wiretap=nil, &block)
|
116
|
+
raise "Block or Wiretap is expected in #{self.class.name}##{__method__}" unless block || wiretap
|
117
|
+
raise "Only Block *or* Wiretap is expected in #{self.class.name}##{__method__}" if block && wiretap
|
118
|
+
@error_handlers << (block || wiretap)
|
119
|
+
if @is_error
|
120
|
+
trigger_error_on(block || wiretap, @is_error)
|
121
|
+
end
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
def trigger_error(error)
|
126
|
+
return if @is_torn_down || @is_completed || @is_error
|
127
|
+
error ||= true
|
128
|
+
@is_error = error
|
129
|
+
|
130
|
+
@error_handlers.each do |block_or_wiretap|
|
131
|
+
trigger_error_on(block_or_wiretap, error)
|
132
|
+
end
|
133
|
+
|
134
|
+
teardown
|
135
|
+
return self
|
136
|
+
end
|
137
|
+
|
138
|
+
def trigger_error_on(block_or_wiretap, error)
|
139
|
+
if block_or_wiretap.is_a? Wiretap
|
140
|
+
block_or_wiretap.trigger_error(error)
|
141
|
+
else
|
142
|
+
enqueue do
|
143
|
+
block_or_wiretap.call(error)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
##|
|
149
|
+
##| Wiretap Predicates
|
150
|
+
##|
|
151
|
+
|
152
|
+
# Returns a Wiretap that will only be called if the &condition block returns
|
153
|
+
# true
|
154
|
+
def filter(&block)
|
155
|
+
return WiretapFilter.new(self, block)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returns a Wiretap that combines all the values into one value (the values
|
159
|
+
# are all passed in at the same time)
|
160
|
+
def combine(&block)
|
161
|
+
return WiretapCombiner.new(self, block)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a Wiretap that passes each value through the block, and also the
|
165
|
+
# previous return value (memo).
|
166
|
+
# @example
|
167
|
+
# wiretap.reduce(0) do |memo, item|
|
168
|
+
# memo + item.price
|
169
|
+
# end
|
170
|
+
# # returns the total of all the prices
|
171
|
+
def reduce(memo=nil, &block)
|
172
|
+
return WiretapReducer.new(self, memo, block)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns a Wiretap that passes each value through the provided block
|
176
|
+
def map(&block)
|
177
|
+
return WiretapMapper.new(self, block)
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
class WiretapTarget < Wiretap
|
183
|
+
attr :target
|
184
|
+
|
185
|
+
def initialize(target, &block)
|
186
|
+
@target = target
|
187
|
+
super(&block)
|
188
|
+
end
|
189
|
+
|
190
|
+
def teardown
|
191
|
+
@target = nil
|
192
|
+
super
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
class WiretapKvo < WiretapTarget
|
198
|
+
attr :property
|
199
|
+
attr :value
|
200
|
+
|
201
|
+
def initialize(target, property, &block)
|
202
|
+
@property = property
|
203
|
+
@value = nil
|
204
|
+
@initial_is_set = false
|
205
|
+
super(target, &block)
|
206
|
+
|
207
|
+
@target.addObserver(self,
|
208
|
+
forKeyPath: property.to_s,
|
209
|
+
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial,
|
210
|
+
context: nil
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
def teardown
|
215
|
+
@target.removeObserver(self,
|
216
|
+
forKeyPath: @property.to_s
|
217
|
+
)
|
218
|
+
@property = nil
|
219
|
+
@value = nil
|
220
|
+
super
|
221
|
+
end
|
222
|
+
|
223
|
+
def trigger_changed(*values)
|
224
|
+
super(@value)
|
225
|
+
end
|
226
|
+
|
227
|
+
def bind_to(wiretap)
|
228
|
+
wiretap.listen do |value|
|
229
|
+
@target.send("#{@property}=", value)
|
230
|
+
end
|
231
|
+
wiretap.trigger_changed
|
232
|
+
end
|
233
|
+
|
234
|
+
def observeValueForKeyPath(path, ofObject: target, change: change, context: context)
|
235
|
+
@value = change[NSKeyValueChangeNewKey]
|
236
|
+
if @initial_is_set
|
237
|
+
trigger_changed(@value)
|
238
|
+
else
|
239
|
+
@initial_is_set = true
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
class WiretapArray < WiretapTarget
|
246
|
+
attr :targets
|
247
|
+
|
248
|
+
def initialize(targets, &block)
|
249
|
+
raise "Not only is listening to an empty array pointless, it will also cause errors" if targets.length == 0
|
250
|
+
|
251
|
+
# the complete trigger isn't called until all the wiretaps are complete
|
252
|
+
@uncompleted = targets.length
|
253
|
+
|
254
|
+
# targets can be an array of Wiretap objects (they will be monitored), or
|
255
|
+
# plain objects (they'll just be included in the sequence)
|
256
|
+
super(targets, &block)
|
257
|
+
|
258
|
+
# gets assigned to the wiretap value if it's a Wiretap, or the object
|
259
|
+
# itself if it is anything else.
|
260
|
+
@value = []
|
261
|
+
@initial_is_set = true
|
262
|
+
# maps the wiretap object (which is unique)
|
263
|
+
@wiretaps = {}
|
264
|
+
|
265
|
+
targets.each_with_index do |wiretap,index|
|
266
|
+
unless wiretap.is_a? Wiretap
|
267
|
+
@value << wiretap
|
268
|
+
# not a wiretap, so doesn't need to be "completed"
|
269
|
+
@uncompleted -= 1
|
270
|
+
else
|
271
|
+
raise "You cannot store a Wiretap twice in the same sequence (for now - do you really need this?)" if @wiretaps.key?(wiretap)
|
272
|
+
@wiretaps[wiretap] = index
|
273
|
+
|
274
|
+
@value << wiretap.value
|
275
|
+
|
276
|
+
wiretap.listen do |value|
|
277
|
+
indx = @wiretaps[wiretap]
|
278
|
+
@value[index] = wiretap.value
|
279
|
+
trigger_changed(*@value)
|
280
|
+
end
|
281
|
+
|
282
|
+
wiretap.on_error do |error|
|
283
|
+
trigger_error(error)
|
284
|
+
end
|
285
|
+
|
286
|
+
wiretap.and_then do |error|
|
287
|
+
@uncompleted -= 1
|
288
|
+
if @uncompleted == 0
|
289
|
+
trigger_completed
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def teardown
|
297
|
+
super
|
298
|
+
@wiretaps = nil
|
299
|
+
end
|
300
|
+
|
301
|
+
def trigger_changed(*values)
|
302
|
+
values = @value if values.length == 0
|
303
|
+
super(*values)
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
|
308
|
+
class WiretapFilter < Wiretap
|
309
|
+
|
310
|
+
def initialize(parent, filter)
|
311
|
+
@parent = parent
|
312
|
+
@filter = filter
|
313
|
+
|
314
|
+
@parent.listen(self)
|
315
|
+
|
316
|
+
super()
|
317
|
+
end
|
318
|
+
|
319
|
+
# passes the values through the filter before passing up to the parent
|
320
|
+
# implementation
|
321
|
+
def trigger_changed(*values)
|
322
|
+
if ( @filter.call(*values) )
|
323
|
+
super(*values)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def teardown
|
328
|
+
super
|
329
|
+
@parent = nil
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
class WiretapCombiner < Wiretap
|
335
|
+
|
336
|
+
def initialize(parent, combiner)
|
337
|
+
@parent = parent
|
338
|
+
@combiner = combiner
|
339
|
+
|
340
|
+
@parent.listen(self)
|
341
|
+
|
342
|
+
super()
|
343
|
+
end
|
344
|
+
|
345
|
+
# passes the values through the combiner before passing up to the parent
|
346
|
+
# implementation
|
347
|
+
def trigger_changed(*values)
|
348
|
+
super(@combiner.call(*values))
|
349
|
+
end
|
350
|
+
|
351
|
+
def teardown
|
352
|
+
super
|
353
|
+
@parent = nil
|
354
|
+
end
|
355
|
+
|
356
|
+
end
|
357
|
+
|
358
|
+
class WiretapReducer < Wiretap
|
359
|
+
|
360
|
+
def initialize(parent, memo, reducer)
|
361
|
+
@parent = parent
|
362
|
+
@reducer = reducer
|
363
|
+
@memo = memo
|
364
|
+
|
365
|
+
@parent.listen(self)
|
366
|
+
|
367
|
+
super()
|
368
|
+
end
|
369
|
+
|
370
|
+
# passes each value through the @reducer, passing in the return value of the
|
371
|
+
# previous call (starting with @memo)
|
372
|
+
def trigger_changed(*values)
|
373
|
+
super(values.inject(@memo, &@reducer))
|
374
|
+
end
|
375
|
+
|
376
|
+
def teardown
|
377
|
+
super
|
378
|
+
@parent = nil
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
383
|
+
class WiretapMapper < Wiretap
|
384
|
+
|
385
|
+
def initialize(parent, mapper)
|
386
|
+
@parent = parent
|
387
|
+
@mapper = mapper
|
388
|
+
|
389
|
+
@parent.listen(self)
|
390
|
+
|
391
|
+
super()
|
392
|
+
end
|
393
|
+
|
394
|
+
# passes the values through the mapper before passing up to the parent
|
395
|
+
# implementation
|
396
|
+
def trigger_changed(*values)
|
397
|
+
super(*values.map { |value| @mapper.call(value) })
|
398
|
+
end
|
399
|
+
|
400
|
+
def teardown
|
401
|
+
super
|
402
|
+
@parent = nil
|
403
|
+
end
|
404
|
+
|
405
|
+
end
|
406
|
+
|
407
|
+
class WiretapProc < WiretapTarget
|
408
|
+
|
409
|
+
def initialize(target, queue=nil, &block)
|
410
|
+
@started = false
|
411
|
+
super(target, &block)
|
412
|
+
queue(queue) if queue
|
413
|
+
end
|
414
|
+
|
415
|
+
def queue(queue)
|
416
|
+
super
|
417
|
+
start
|
418
|
+
end
|
419
|
+
|
420
|
+
def start
|
421
|
+
unless @started
|
422
|
+
@started = true
|
423
|
+
enqueue do
|
424
|
+
begin
|
425
|
+
trigger_changed(@target.call)
|
426
|
+
rescue Exception => error
|
427
|
+
trigger_error(error)
|
428
|
+
else
|
429
|
+
trigger_completed
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
|
436
|
+
def listen(wiretap=nil, &block)
|
437
|
+
raise "WiretapProc does not support listeners (only the `completed` event is triggered)"
|
438
|
+
end
|
439
|
+
|
440
|
+
def and_then(wiretap=nil, &block)
|
441
|
+
super
|
442
|
+
start
|
443
|
+
end
|
444
|
+
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class NSObject
|
2
|
+
attr_accessor :motion_wiretap_observers
|
3
|
+
|
4
|
+
def wiretap(property, &block)
|
5
|
+
MotionWiretap::WiretapKvo.new(self, property, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
class NSArray
|
12
|
+
|
13
|
+
def wiretap(property=nil, &block)
|
14
|
+
raise "`wiretap` is not supported on Arrays (arrays are not observable). You probably meant to use `wiretaps`."
|
15
|
+
end
|
16
|
+
|
17
|
+
def wiretaps(&block)
|
18
|
+
MotionWiretap::WiretapArray.new(self, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class Proc
|
25
|
+
|
26
|
+
def wiretap(queue=nil, &block)
|
27
|
+
MotionWiretap::WiretapProc(queue, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module MotionWiretap
|
2
|
+
class ControlEventNotFound < Exception
|
3
|
+
end
|
4
|
+
|
5
|
+
# Some UIControlEvent translators. Based on SugarCube's uicontroleevent
|
6
|
+
# constants.
|
7
|
+
module ControlEvents
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def convert(control_events)
|
11
|
+
return control_events if control_events.is_a? Fixnum
|
12
|
+
|
13
|
+
case control_events
|
14
|
+
when NSArray
|
15
|
+
retval = 0
|
16
|
+
control_events.each do |event|
|
17
|
+
begin
|
18
|
+
retval |= ControlEvents.convert(event)
|
19
|
+
rescue ControlEventNotFound
|
20
|
+
raise "Could not merge control event #{event.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
return retval
|
24
|
+
when :touch
|
25
|
+
return UIControlEventTouchUpInside
|
26
|
+
when :touch_up
|
27
|
+
return UIControlEventTouchUpInside
|
28
|
+
when :touch_down
|
29
|
+
return UIControlEventTouchDown
|
30
|
+
when :touch_start
|
31
|
+
return UIControlEventTouchDown | UIControlEventTouchDragEnter
|
32
|
+
when :touch_stop
|
33
|
+
return UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit
|
34
|
+
when :change
|
35
|
+
return UIControlEventValueChanged | UIControlEventEditingChanged
|
36
|
+
when :begin
|
37
|
+
return UIControlEventEditingDidBegin
|
38
|
+
when :end
|
39
|
+
return UIControlEventEditingDidEnd
|
40
|
+
when :touch_down_repeat
|
41
|
+
return UIControlEventTouchDownRepeat
|
42
|
+
when :touch_drag_inside
|
43
|
+
return UIControlEventTouchDragInside
|
44
|
+
when :touch_drag_outside
|
45
|
+
return UIControlEventTouchDragOutside
|
46
|
+
when :touch_drag_enter
|
47
|
+
return UIControlEventTouchDragEnter
|
48
|
+
when :touch_drag_exit
|
49
|
+
return UIControlEventTouchDragExit
|
50
|
+
when :touch_up_inside
|
51
|
+
return UIControlEventTouchUpInside
|
52
|
+
when :touch_up_outside
|
53
|
+
return UIControlEventTouchUpOutside
|
54
|
+
when :touch_cancel
|
55
|
+
return UIControlEventTouchCancel
|
56
|
+
when :value_changed
|
57
|
+
return UIControlEventValueChanged
|
58
|
+
when :editing_did_begin
|
59
|
+
return UIControlEventEditingDidBegin
|
60
|
+
when :editing_changed
|
61
|
+
return UIControlEventEditingChanged
|
62
|
+
when :editing_did_change
|
63
|
+
return UIControlEventEditingChanged
|
64
|
+
when :editing_did_end
|
65
|
+
return UIControlEventEditingDidEnd
|
66
|
+
when :editing_did_end_on_exit
|
67
|
+
return UIControlEventEditingDidEndOnExit
|
68
|
+
when :all_touch
|
69
|
+
return UIControlEventAllTouchEvents
|
70
|
+
when :all_editing
|
71
|
+
return UIControlEventAllEditingEvents
|
72
|
+
when :application
|
73
|
+
return UIControlEventApplicationReserved
|
74
|
+
when :system
|
75
|
+
return UIControlEventSystemReserved
|
76
|
+
when :all
|
77
|
+
return UIControlEventAllEvents
|
78
|
+
else
|
79
|
+
raise ControlEventNotFound.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class UIView
|
2
|
+
|
3
|
+
def wiretap(property=nil, &block)
|
4
|
+
if property.nil?
|
5
|
+
MotionWiretap::WiretapView.new(self, &block)
|
6
|
+
else
|
7
|
+
MotionWiretap::WiretapKvo.new(self, property, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
class UIControl
|
15
|
+
|
16
|
+
def wiretap(property=nil, &block)
|
17
|
+
if property.nil?
|
18
|
+
MotionWiretap::WiretapControl.new(self, &block)
|
19
|
+
else
|
20
|
+
MotionWiretap::WiretapKvo.new(self, property, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module MotionWiretap
|
2
|
+
# Some gesture recognizer translators. Based on SugarCube's gesture recognizer
|
3
|
+
# methods.
|
4
|
+
module Gestures
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# @yield [recognizer] Handles the gesture event, and passes the recognizer instance to the block.
|
8
|
+
# @overload tap(taps)
|
9
|
+
# @param taps [Fixnum] Number of taps
|
10
|
+
# @overload tap(options)
|
11
|
+
# @option options [Fixnum] :taps Number of taps before gesture is recognized
|
12
|
+
# @option options [Fixnum] :fingers Number of fingers before gesture is recognized
|
13
|
+
def tap(target, taps_or_options=nil)
|
14
|
+
taps = nil
|
15
|
+
fingers = nil
|
16
|
+
|
17
|
+
if taps_or_options
|
18
|
+
if taps_or_options.is_a? Hash
|
19
|
+
taps = taps_or_options[:taps] || taps
|
20
|
+
fingers = taps_or_options[:fingers] || fingers
|
21
|
+
else
|
22
|
+
taps = taps_or_options
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
recognizer = UITapGestureRecognizer.alloc.initWithTarget(target, action:'handle_gesture:')
|
27
|
+
recognizer.numberOfTapsRequired = taps if taps
|
28
|
+
recognizer.numberOfTouchesRequired = fingers if fingers
|
29
|
+
return recognizer
|
30
|
+
end
|
31
|
+
|
32
|
+
# @yield [recognizer] Handles the gesture event, and passes the recognizer instance to the block.
|
33
|
+
def pinch(target)
|
34
|
+
recognizer = UIPinchGestureRecognizer.alloc.initWithTarget(target, action:'handle_gesture:')
|
35
|
+
return recognizer
|
36
|
+
end
|
37
|
+
|
38
|
+
# @yield [recognizer] Handles the gesture event, and passes the recognizer instance to the block.
|
39
|
+
def rotate(target)
|
40
|
+
recognizer = UIRotationGestureRecognizer.alloc.initWithTarget(target, action:'handle_gesture:')
|
41
|
+
return recognizer
|
42
|
+
end
|
43
|
+
|
44
|
+
# @yield [recognizer] Handles the gesture event, and passes the recognizer instance to the block.
|
45
|
+
# @overload swipe(taps)
|
46
|
+
# @param direction [Fixnum] Direction of swipe
|
47
|
+
# @overload swipe(options)
|
48
|
+
# @option options [Fixnum] :fingers Number of fingers before gesture is recognized
|
49
|
+
# @option options [Fixnum, Symbol] :direction Direction of swipe, as a UISwipeGestureRecognizerDirection constant or a symbol (`:left, :right, :up, :down`)
|
50
|
+
def swipe(target, direction_or_options)
|
51
|
+
direction = nil
|
52
|
+
fingers = nil
|
53
|
+
|
54
|
+
if direction_or_options
|
55
|
+
if direction_or_options.is_a? Hash
|
56
|
+
direction = direction_or_options[:direction] || direction
|
57
|
+
fingers = direction_or_options[:fingers] || fingers
|
58
|
+
else
|
59
|
+
direction = direction_or_options
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
case direction
|
64
|
+
when :left
|
65
|
+
direction = UISwipeGestureRecognizerDirectionLeft
|
66
|
+
when :right
|
67
|
+
direction = UISwipeGestureRecognizerDirectionRight
|
68
|
+
when :up
|
69
|
+
direction = UISwipeGestureRecognizerDirectionUp
|
70
|
+
when :down
|
71
|
+
direction = UISwipeGestureRecognizerDirectionDown
|
72
|
+
end
|
73
|
+
|
74
|
+
recognizer = UISwipeGestureRecognizer.alloc.initWithTarget(target, action:'handle_gesture:')
|
75
|
+
recognizer.direction = direction if direction
|
76
|
+
recognizer.numberOfTouchesRequired = fingers if fingers
|
77
|
+
return recognizer
|
78
|
+
end
|
79
|
+
|
80
|
+
# @yield [recognizer] Handles the gesture event, and passes the recognizer instance to the block.
|
81
|
+
# @overload tap(taps)
|
82
|
+
# @param taps [Fixnum] Number of taps
|
83
|
+
# @overload tap(options)
|
84
|
+
# @option options [Fixnum] :min_fingers Minimum umber of fingers for gesture to be recognized
|
85
|
+
# @option options [Fixnum] :max_fingers Maximum number of fingers for gesture to be recognized
|
86
|
+
# @option options [Fixnum] :fingers If min_fingers or max_fingers is not assigned, this will be the default.
|
87
|
+
def pan(target, fingers_or_options=nil)
|
88
|
+
fingers = nil
|
89
|
+
min_fingers = nil
|
90
|
+
max_fingers = nil
|
91
|
+
|
92
|
+
if fingers_or_options
|
93
|
+
if fingers_or_options.is_a? Hash
|
94
|
+
fingers = fingers_or_options[:fingers] || fingers
|
95
|
+
min_fingers = fingers_or_options[:min_fingers] || min_fingers
|
96
|
+
max_fingers = fingers_or_options[:max_fingers] || max_fingers
|
97
|
+
else
|
98
|
+
fingers = fingers_or_options
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# if fingers is assigned, but not min/max, assign it as a default
|
103
|
+
min_fingers ||= fingers
|
104
|
+
max_fingers ||= fingers
|
105
|
+
|
106
|
+
recognizer = UIPanGestureRecognizer.alloc.initWithTarget(target, action:'handle_gesture:')
|
107
|
+
recognizer.maximumNumberOfTouches = min_fingers if min_fingers
|
108
|
+
recognizer.minimumNumberOfTouches = max_fingers if max_fingers
|
109
|
+
return recognizer
|
110
|
+
end
|
111
|
+
|
112
|
+
# @yield [recognizer] Handles the gesture event, and passes the recognizer instance to the block.
|
113
|
+
# @overload press(duration)
|
114
|
+
# @param duration [Fixnum] How long in seconds before gesture is recognized
|
115
|
+
# @overload press(options)
|
116
|
+
# @option options [Fixnum] :duration How long in seconds before gesture is recognized
|
117
|
+
# @option options [Fixnum] :taps Number of taps before gesture is recognized
|
118
|
+
# @option options [Fixnum] :fingers Number of fingers before gesture is recognized
|
119
|
+
def press(target, duration_or_options=nil)
|
120
|
+
duration = nil
|
121
|
+
taps = nil
|
122
|
+
fingers = nil
|
123
|
+
|
124
|
+
if duration_or_options
|
125
|
+
if duration_or_options.is_a? Hash
|
126
|
+
duration = duration_or_options[:duration] || duration
|
127
|
+
taps = duration_or_options[:taps] || taps
|
128
|
+
fingers = duration_or_options[:fingers] || fingers
|
129
|
+
else
|
130
|
+
duration = duration_or_options
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
recognizer = UILongPressGestureRecognizer.alloc.initWithTarget(target, action:'handle_gesture:')
|
135
|
+
recognizer.minimumPressDuration = duration if duration
|
136
|
+
recognizer.numberOfTapsRequired = taps if taps
|
137
|
+
recognizer.numberOfTouchesRequired = fingers if fingers
|
138
|
+
return recognizer
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module MotionWiretap
|
2
|
+
|
3
|
+
class WiretapView < WiretapTarget
|
4
|
+
|
5
|
+
def on(recognizer, options=nil, &block)
|
6
|
+
case recognizer
|
7
|
+
when :tap
|
8
|
+
recognizer = MotionWiretap::Gestures.tap(self, options)
|
9
|
+
when :pinch
|
10
|
+
recognizer = MotionWiretap::Gestures.pinch(self, options)
|
11
|
+
when :rotate
|
12
|
+
recognizer = MotionWiretap::Gestures.rotate(self, options)
|
13
|
+
when :swipe
|
14
|
+
recognizer = MotionWiretap::Gestures.swipe(self, options)
|
15
|
+
when :pan
|
16
|
+
recognizer = MotionWiretap::Gestures.pan(self, options)
|
17
|
+
when :press
|
18
|
+
recognizer = MotionWiretap::Gestures.press(self, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
self.target.addGestureRecognizer(recognizer)
|
22
|
+
listen(&block) if block
|
23
|
+
return self
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_gesture(recognizer)
|
27
|
+
trigger_changed
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class WiretapControl < WiretapView
|
33
|
+
|
34
|
+
# control_event can be any UIControlEventconstant, or any symbol found in
|
35
|
+
# wiretap_control_events.rb, or an array of UIControlEvent constants or
|
36
|
+
# symbols.
|
37
|
+
def on(control_event, options={}, &block)
|
38
|
+
begin
|
39
|
+
control_event = ControlEvents.convert(control_event)
|
40
|
+
self.target.addTarget(self, action:'handle_event:', forControlEvents:control_event)
|
41
|
+
listen(&block) if block
|
42
|
+
rescue ControlEventNotFound
|
43
|
+
super(control_event, options, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
return self
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_event(event)
|
50
|
+
trigger_changed
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/motion-wiretap/version.rb', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'motion-wiretap'
|
6
|
+
gem.version = MotionWiretap::Version
|
7
|
+
|
8
|
+
gem.authors = ['Colin T.A. Gray']
|
9
|
+
gem.email = ['colinta@gmail.com']
|
10
|
+
gem.summary = %{It's like ReactiveCocoa, but in RubyMotion}
|
11
|
+
gem.description = <<-DESC
|
12
|
+
ReactiveCocoa is an amazing system, and RubyMotion could benefit from the
|
13
|
+
lessons learned there!
|
14
|
+
|
15
|
+
Motion-Wiretap is, essentially, a wrapper for Key-Value coding and observation.
|
16
|
+
It exposes a +Wiretap+ class that you can use as a signal, or add listeners to
|
17
|
+
it.
|
18
|
+
|
19
|
+
Extensions are provided to listen to an +Array+ of +Wiretap+ objects, and the
|
20
|
+
`UIKit`/`AppKit` classes are augmented to provide actions as events (gestures,
|
21
|
+
mouse events, value changes).
|
22
|
+
DESC
|
23
|
+
|
24
|
+
gem.homepage = 'https://github.com/colinta/motion-wiretap'
|
25
|
+
|
26
|
+
gem.files = Dir.glob('lib/motion-wiretap/**/*.rb') + ['README.md', 'motion-wiretap.gemspec']
|
27
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
28
|
+
|
29
|
+
gem.require_paths = ['lib']
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: motion-wiretap
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Colin T.A. Gray
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-16 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! 'ReactiveCocoa is an amazing system, and RubyMotion could benefit from
|
15
|
+
the
|
16
|
+
|
17
|
+
lessons learned there!
|
18
|
+
|
19
|
+
|
20
|
+
Motion-Wiretap is, essentially, a wrapper for Key-Value coding and observation.
|
21
|
+
|
22
|
+
It exposes a +Wiretap+ class that you can use as a signal, or add listeners to
|
23
|
+
|
24
|
+
it.
|
25
|
+
|
26
|
+
|
27
|
+
Extensions are provided to listen to an +Array+ of +Wiretap+ objects, and the
|
28
|
+
|
29
|
+
`UIKit`/`AppKit` classes are augmented to provide actions as events (gestures,
|
30
|
+
|
31
|
+
mouse events, value changes).
|
32
|
+
|
33
|
+
'
|
34
|
+
email:
|
35
|
+
- colinta@gmail.com
|
36
|
+
executables: []
|
37
|
+
extensions: []
|
38
|
+
extra_rdoc_files: []
|
39
|
+
files:
|
40
|
+
- lib/motion-wiretap/all/signal.rb
|
41
|
+
- lib/motion-wiretap/all/wiretap.rb
|
42
|
+
- lib/motion-wiretap/all/wiretap_exts.rb
|
43
|
+
- lib/motion-wiretap/ios/wiretap_control_events.rb
|
44
|
+
- lib/motion-wiretap/ios/wiretap_exts_ios.rb
|
45
|
+
- lib/motion-wiretap/ios/wiretap_gestures.rb
|
46
|
+
- lib/motion-wiretap/ios/wiretap_ios.rb
|
47
|
+
- lib/motion-wiretap/osx/wiretap_exts_osx.rb
|
48
|
+
- lib/motion-wiretap/osx/wiretap_osx.rb
|
49
|
+
- lib/motion-wiretap/version.rb
|
50
|
+
- README.md
|
51
|
+
- motion-wiretap.gemspec
|
52
|
+
homepage: https://github.com/colinta/motion-wiretap
|
53
|
+
licenses: []
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.8.25
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: It's like ReactiveCocoa, but in RubyMotion
|
76
|
+
test_files: []
|