motion-wiretap 0.1.0 → 0.2.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 CHANGED
@@ -1,16 +1,156 @@
1
- MotionWiretap
1
+ Wiretap
2
2
  -------
3
3
 
4
4
  An iOS / OS X wrapper heavily inspired by ReactiveCocoa.
5
5
 
6
- gem coming soon, just need to write the gemspec
6
+ gem install motion-wiretap
7
7
 
8
- run the OSX specs using `rake spec platform=osx`
8
+ Run the OSX specs using `rake spec platform=osx`
9
+
10
+ First things first
11
+ ------------------
12
+
13
+ You **MUST** call `cancel!` on any wiretaps you create, otherwise they will live
14
+ in memory forever. The upside is that you can create
9
15
 
10
16
  Showdown
11
- -------
17
+ --------
12
18
 
13
19
  Open these side by side to see the comparison:
14
20
 
15
21
  [reactive.rb](https://gist.github.com/colinta/d0a273f8d858a8f61c73)
16
22
  [reactive.mm](https://gist.github.com/colinta/5cfa588fed7b929193ae)
23
+
24
+ Usage
25
+ -----
26
+
27
+ These are taken from the specs.
28
+
29
+ ### Key-Value Observation
30
+
31
+ ```ruby
32
+ class Person
33
+ attr_accessor :name
34
+ attr_accessor :email
35
+ attr_accessor :address
36
+ end
37
+
38
+ # listen for changes
39
+ person = Person.new
40
+ person.wiretap(:name) do |name|
41
+ puts "name is now #{name}"
42
+ end
43
+ person.name = 'Joe'
44
+ # puts => "name is now Joe"
45
+
46
+ # bind the property of one object to the value of another
47
+ person_1 = Person.new
48
+ person_2 = Person.new
49
+ wiretap = person_1.wiretap(:name).bind_to(person_2.wiretap(:name))
50
+ person_2.name = 'Bob'
51
+ person_1.name # => "Bob"
52
+
53
+ # cancel a wiretap
54
+ wiretap.cancel!
55
+ person_2.name = 'Jane'
56
+ person_1.name
57
+ # => "BOB"
58
+
59
+ # bind the property of one object to the value of another, but change it using `map`
60
+ wiretap = person_1.wiretap(:name).bind_to(person_2.wiretap(:name).map { |value| value.upcase })
61
+ person_2.name = 'Bob'
62
+ person_1.name # => "BOB"
63
+ ```
64
+
65
+ ### Working with arrays
66
+
67
+ ```
68
+ # combine the values `name` and `email`
69
+ person = Person.new
70
+ [
71
+ person.wiretap(:name),
72
+ person.wiretap(:email),
73
+ ].wiretap.combine do |name, email|
74
+ puts "#{name} <#{email}>"
75
+ end
76
+
77
+ person.name = 'Kazuo'
78
+ # puts => "Kazuo <>"
79
+ person.email = 'kaz@example.com'
80
+ # puts => "Kazuo <kaz@example.com>"
81
+
82
+ # reduce/inject
83
+ person_1 = Person.new
84
+ person_2 = Person.new
85
+ [
86
+ person_1.wiretap(:name),
87
+ person_2.wiretap(:name),
88
+ ].wiretap.reduce do |memo, name|
89
+ memo ||= []
90
+ memo + [name]
91
+ end
92
+ person_1.name = 'Mr. White'
93
+ person_1.name = 'Mr. Blue'
94
+ # => ['Mr. White', 'Mr. Blue']
95
+
96
+ # you can provide an initial 'memo' (default: nil)
97
+ [
98
+ person_1.wiretap(:name),
99
+ person_2.wiretap(:name),
100
+ ].wiretap.reduce([]) do |memo, name|
101
+ memo + [name] # you should not change memo in place, the same one will be used on every change event
102
+ end
103
+ ```
104
+
105
+ ### Monitoring jobs
106
+
107
+ ```ruby
108
+ # monitor for background job completion
109
+ -> {
110
+ this_will_take_forever!
111
+ }.wiretap(Dispatch::Queue.concurrent) do
112
+ puts "done!"
113
+ end
114
+
115
+ # Note: this is convenient shorthand for calling `queue`, `and_then`, and `start`
116
+ -> {}.wiretap.queue(...).and_then do ... end.start
117
+
118
+ # send a stream of values. a lambda is passed in that will forward change
119
+ # events to the `listen` block
120
+ -> (on_change) do
121
+ 5.times do |count|
122
+ on_change.call count
123
+ end
124
+ end.wiretap.queue(Dispatch::Queue.concurrent).listen do |index|
125
+ puts "...#{index}"
126
+ end.and_then do
127
+ puts "done!"
128
+ end.start
129
+ # => puts "...0", "...1", "...2", "...3", "...4", "done!"
130
+ ```
131
+
132
+ ### Let's do something practical!
133
+
134
+ ```ruby
135
+ # bind the `enabled` property to the Wiretap object to a check on whether
136
+ # `username_field.text` and `password_field.text` are blank
137
+ @login_button.wiretap(:enabled).bind_to(
138
+ [
139
+ @username_field.wiretap(:text),
140
+ @password_field.wiretap(:text),
141
+ ].wiretap.combine do |username, password|
142
+ username && username.length > 0 && password && password.length > 0
143
+ end
144
+ )
145
+ ```
146
+
147
+ ### Notifications
148
+
149
+ ```ruby
150
+ notification = "NotificationName".wiretap do
151
+ puts "notification received!"
152
+ end
153
+
154
+ # rememder: it's *important* to cancel all wiretaps!
155
+ notification.cancel!
156
+ ```
@@ -1,11 +1,10 @@
1
- $motion_wiretaps = []
2
1
 
3
2
  module MotionWiretap
4
3
 
5
4
  class Wiretap
6
5
 
7
6
  def initialize(&block)
8
- $motion_wiretaps << self # signal will be removed when it is completed
7
+ # MotionWiretap.wiretaps << self # signal will be removed when it is completed
9
8
 
10
9
  @is_torn_down = false
11
10
  @is_completed = false
@@ -19,16 +18,16 @@ module MotionWiretap
19
18
  listen &block if block
20
19
  end
21
20
 
22
- def dealloc
21
+ # this is the preferred way to turn off a wiretap; child classes override
22
+ # `teardown`, which is only ever called once.
23
+ def cancel!
24
+ return if @is_torn_down
25
+ @is_torn_down = true
23
26
  teardown
24
- super
25
27
  end
26
28
 
29
+ # for internal use
27
30
  def teardown
28
- return if @is_torn_down
29
-
30
- @is_torn_down = true
31
- $motion_wiretaps.remove(self)
32
31
  end
33
32
 
34
33
  # specify the GCD queue that the listeners should be run on
@@ -97,7 +96,7 @@ module MotionWiretap
97
96
  trigger_completed_on(block_or_wiretap)
98
97
  end
99
98
 
100
- teardown
99
+ cancel!
101
100
  return self
102
101
  end
103
102
 
@@ -131,7 +130,7 @@ module MotionWiretap
131
130
  trigger_error_on(block_or_wiretap, error)
132
131
  end
133
132
 
134
- teardown
133
+ cancel!
135
134
  return self
136
135
  end
137
136
 
@@ -242,18 +241,18 @@ module MotionWiretap
242
241
 
243
242
  end
244
243
 
245
- class WiretapArray < WiretapTarget
244
+ class WiretapArray < Wiretap
246
245
  attr :targets
247
246
 
248
247
  def initialize(targets, &block)
249
248
  raise "Not only is listening to an empty array pointless, it will also cause errors" if targets.length == 0
250
249
 
251
- # the complete trigger isn't called until all the wiretaps are complete
250
+ # the complete trigger isn't called until all the wiretap are complete
252
251
  @uncompleted = targets.length
253
252
 
254
253
  # targets can be an array of Wiretap objects (they will be monitored), or
255
254
  # plain objects (they'll just be included in the sequence)
256
- super(targets, &block)
255
+ super(&block)
257
256
 
258
257
  # gets assigned to the wiretap value if it's a Wiretap, or the object
259
258
  # itself if it is anything else.
@@ -293,9 +292,16 @@ module MotionWiretap
293
292
  end
294
293
  end
295
294
 
296
- def teardown
295
+ def cancel!
296
+ @wiretaps.each do |wiretap,index|
297
+ wiretap.cancel!
298
+ end
297
299
  super
300
+ end
301
+
302
+ def teardown
298
303
  @wiretaps = nil
304
+ super
299
305
  end
300
306
 
301
307
  def trigger_changed(*values)
@@ -325,8 +331,8 @@ module MotionWiretap
325
331
  end
326
332
 
327
333
  def teardown
328
- super
329
334
  @parent = nil
335
+ super
330
336
  end
331
337
 
332
338
  end
@@ -349,8 +355,8 @@ module MotionWiretap
349
355
  end
350
356
 
351
357
  def teardown
352
- super
353
358
  @parent = nil
359
+ super
354
360
  end
355
361
 
356
362
  end
@@ -374,8 +380,8 @@ module MotionWiretap
374
380
  end
375
381
 
376
382
  def teardown
377
- super
378
383
  @parent = nil
384
+ super
379
385
  end
380
386
 
381
387
  end
@@ -398,23 +404,21 @@ module MotionWiretap
398
404
  end
399
405
 
400
406
  def teardown
401
- super
402
407
  @parent = nil
408
+ super
403
409
  end
404
410
 
405
411
  end
406
412
 
407
413
  class WiretapProc < WiretapTarget
408
414
 
409
- def initialize(target, queue=nil, &block)
415
+ def initialize(target, queue, block)
410
416
  @started = false
411
- super(target, &block)
417
+ super(target)
418
+ and_then(&block) if block
412
419
  queue(queue) if queue
413
- end
414
420
 
415
- def queue(queue)
416
- super
417
- start
421
+ start if block
418
422
  end
419
423
 
420
424
  def start
@@ -422,7 +426,11 @@ module MotionWiretap
422
426
  @started = true
423
427
  enqueue do
424
428
  begin
425
- trigger_changed(@target.call)
429
+ if @target.arity == 0
430
+ @target.call
431
+ else
432
+ @target.call(-> (value) { self.trigger_changed(value) })
433
+ end
426
434
  rescue Exception => error
427
435
  trigger_error(error)
428
436
  else
@@ -432,16 +440,33 @@ module MotionWiretap
432
440
  end
433
441
  end
434
442
 
443
+ end
435
444
 
436
- def listen(wiretap=nil, &block)
437
- raise "WiretapProc does not support listeners (only the `completed` event is triggered)"
445
+ class WiretapNotification < Wiretap
446
+
447
+ def initialize(notification, object, block)
448
+ @notification = notification
449
+ @object = object
450
+
451
+ NSNotificationCenter.defaultCenter.addObserver(self, selector: 'notify:', name:@notification, object:@object)
452
+ listen(&block) if block
438
453
  end
439
454
 
440
- def and_then(wiretap=nil, &block)
455
+ def notify(notification)
456
+ trigger_changed(notification.object, notification.userInfo)
457
+ end
458
+
459
+ def teardown
460
+ NSNotificationCenter.defaultCenter.removeObserver(self, name:@notification, object:@object)
441
461
  super
442
- start
443
462
  end
444
463
 
445
464
  end
446
465
 
466
+ module_function
467
+
468
+ def wiretaps
469
+ @wiretaps ||= []
470
+ end
471
+
447
472
  end
@@ -10,11 +10,7 @@ end
10
10
 
11
11
  class NSArray
12
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)
13
+ def wiretap(&block)
18
14
  MotionWiretap::WiretapArray.new(self, &block)
19
15
  end
20
16
 
@@ -24,7 +20,16 @@ end
24
20
  class Proc
25
21
 
26
22
  def wiretap(queue=nil, &block)
27
- MotionWiretap::WiretapProc(queue, &block)
23
+ MotionWiretap::WiretapProc.new(self, queue, block)
24
+ end
25
+
26
+ end
27
+
28
+
29
+ class NSString
30
+
31
+ def wiretap(object=nil, &block)
32
+ MotionWiretap::WiretapNotification(self, object, block)
28
33
  end
29
34
 
30
35
  end
@@ -76,7 +76,7 @@ module MotionWiretap
76
76
  when :all
77
77
  return UIControlEventAllEvents
78
78
  else
79
- raise ControlEventNotFound.new
79
+ raise ControlEventNotFound.new(control_events.to_s)
80
80
  end
81
81
  end
82
82
 
@@ -1,25 +1,33 @@
1
1
  module MotionWiretap
2
+ class GestureNotFound < Exception
3
+ end
2
4
 
3
- class WiretapView < WiretapTarget
5
+ class WiretapView < Wiretap
4
6
 
5
7
  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)
8
+ if recognizer
9
+ case recognizer
10
+ when :tap
11
+ recognizer = Gestures.tap(self, options)
12
+ when :pinch
13
+ recognizer = Gestures.pinch(self, options)
14
+ when :rotate
15
+ recognizer = Gestures.rotate(self, options)
16
+ when :swipe
17
+ recognizer = Gestures.swipe(self, options)
18
+ when :pan
19
+ recognizer = Gestures.pan(self, options)
20
+ when :press
21
+ recognizer = Gestures.press(self, options)
22
+ else
23
+ raise GestureNotFound.new(recognizer.to_s)
24
+ end
25
+
26
+ self.target.addGestureRecognizer(recognizer)
19
27
  end
20
28
 
21
- self.target.addGestureRecognizer(recognizer)
22
- listen(&block) if block
29
+ super(&block)
30
+
23
31
  return self
24
32
  end
25
33
 
@@ -31,16 +39,18 @@ module MotionWiretap
31
39
 
32
40
  class WiretapControl < WiretapView
33
41
 
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.
42
+ # control_event can be any UIControlEventConstant, or any symbol found in
43
+ # wiretap_control_events.rb, or an array of UIControlEventConstants or
44
+ # symbols. Since UIView implements `on` to accept a gesture, this method
45
+ # calls `super` when the symbol isn't a control
37
46
  def on(control_event, options={}, &block)
38
47
  begin
39
48
  control_event = ControlEvents.convert(control_event)
40
49
  self.target.addTarget(self, action:'handle_event:', forControlEvents:control_event)
41
- listen(&block) if block
42
50
  rescue ControlEventNotFound
43
51
  super(control_event, options, &block)
52
+ else
53
+ super(nil, options, &block)
44
54
  end
45
55
 
46
56
  return self
@@ -1,3 +1,3 @@
1
1
  module MotionWiretap
2
- Version = '0.1.0'
2
+ Version = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motion-wiretap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-16 00:00:00.000000000 Z
12
+ date: 2013-09-24 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ! 'ReactiveCocoa is an amazing system, and RubyMotion could benefit from
15
15
  the