motion-wiretap 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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