MotionWiretap
An iOS / OS X library heavily inspired by ReactiveCocoa.
gem install motion-wiretap
Run the iOS specs using rake spec
Run the OSX specs using rake spec platform=osx
First things first
You MUST retain any wiretaps you create, otherwise they will be garbage collected immediately. I don't yet have a clever way to avoid this unfortunate state of affairs, other than keeping some global list of wiretaps. UG to that.
How does ReactiveCocoa do it, anyone know? I'd love to mimic that.
This isn't a terrible thing, though, since most wiretaps require that you call
the cancel!
method. Again, I'm not sure how ReactiveCocoa accomplishes the
"auto shutdown" of signals that rely on notifications and key-value observation.
Showdown
Open these side by side to see the comparison:
Usage
Creating a wiretap is done using the factory method Motion.wiretap()
.
@wiretap = Motion.wiretap(obj, :property)
If you want to use a more literate style, you can include the
motion-wiretap-polluting
to have a wiretap
method added to your objects. I
like this style, but the default is to have a non-polluting system.
@wiretap = obj.wiretap(:property)
Wiretaps can be composed all sorts of ways: you can filter them, map them, combine multiple wiretaps into a single value, use them to respond to notifications or control events.
Let's start with something practical!
This first example will use motion-wiretap-polluting
methods, because that's
my preferred aesthetic style, but the rest of this document will focus on the
non-polluting version.
# assign the label to the text view; changes to the text view will be reflected
# on the label.
@label.wiretap(:text).bind_to(@text_view.wiretap(:text))
# assign the attributedText of the label to the text view, doing some
# highlighting in-between
@label.wiretap(:attributedText).bind_to(@text_view.wiretap(:text).map do |text|
NSAttributedString.alloc.initWithString(text, attributes: { NSForegroundColorAttributeName => UIColor.blueColor })
end)
# bind the `enabled` property to check on whether `username_field.text` and
# `password_field.text` are blank
@login_button.wiretap(:enabled).bind_to(
[
@username_field.wiretap(:text),
@password_field.wiretap(:text),
].wiretap.combine do |username, password|
username && username.length > 0 && password && password.length > 0
end
)
Types of wiretaps
- Key-Value Observation / KVO
- Arrays (map/reduce)
- Jobs (event stream, completion)
- NSNotificationCenter
- UIView Gestures
- UIControl events
Key-Value Observation
class Person
attr_accessor :name
attr_accessor :email
attr_accessor :address
end
# listen for changes
person = Person.new
# you need to store the wiretap object in memory; when the object is
# deallocated, it will no longer receive updates.
wiretap = Motion.wiretap(person, :name)
# react to change events
wiretap.listen do |name|
puts "name is now #{name}"
end
person.name = 'Joe'
# puts => "name is now Joe"
# Since listening is very common, you can easily shorthand this to:
wiretap = Motion.wiretap(person, :name) do |name|
puts "name is now #{name}"
end
# bind the property of one object to the value of another
person_1 = Person.new
person_2 = Person.new
wiretap = Motion.wiretap(person_1, :name)
wiretap.bind_to(person_2, :name) # creates a new Wiretap object for person_2; changes to person_2.name will affect person_1
person_2.name = 'Bob'
person_1.name # => "Bob"
# cancel a wiretap
wiretap.cancel!
person_2.name = 'Jane'
person_1.name
# => "BOB"
# bind the property of one object to the value of another, but change it using `map`
wiretap = Motion.wiretap(person_1, :name).bind_to(person_2.wiretap(:name).map { |value| value.upcase })
person_2.name = 'Bob'
person_1.name # => "BOB"
Working with arrays
# combine the values `name` and `email`
person = Person.new
taps = Motion.wiretap([
Motion.wiretap(person, :name),
Motion.wiretap(person, :email),
]).combine do |name, email|
puts "#{name} <#{email}>"
end
person.name = 'Kazuo'
# puts => "Kazuo <>"
person.email = '[email protected]'
# puts => "Kazuo <[email protected]>"
# reduce/inject
person_1 = Person.new
person_2 = Person.new
taps = Motion.wiretap([
Motion.wiretap(person_1, :name),
Motion.wiretap(person_2, :name),
]).reduce do |memo, name|
memo ||= []
memo + [name]
end
wiretap.listen do |names|
puts names.inspect
end
person_1.name = 'Mr. White'
person_1.name = 'Mr. Blue'
# => ["Mr. White", "Mr. Blue"]
# you can provide an initial 'memo' (default: nil)
Motion.wiretap([
Motion.wiretap(person_1, :name),
Motion.wiretap(person_2, :name),
]).reduce([]) do |memo, name|
memo + [name] # you should not change memo in place, the same one will be used on every change event
end
Monitoring jobs
# Monitor for background job completion:
Motion.wiretap(-> do
this_will_take_forever!
end).and_then do
puts "done!"
end.start
# Same, but specifying the thread to run on. The completion block will be called
# on this thread, too. The queue conveniently accepts a completion handler
# (delegates to the `Wiretap#and_then` method).
Motion.wiretap(-> do
this_will_take_forever!
end).queue(Dispatch::Queue.concurrent).and_then do
puts "done!"
end.start
# Send a stream of values from a block. A lambda is passed in that will forward
# change events to the `listen` block
Motion.wiretap(-> (on_change) do
5.times do |count|
on_change.call count
end
end).listen do |index|
puts "...#{index}"
end.and_then do
puts "done!"
end.start
# => puts "...0", "...1", "...2", "...3", "...4", "done!"
Notifications
notification = Motion.wiretap('NotificationName') do
puts 'notification received!'
end
# the notification observer will be removed when the Wiretap is dealloc'd
Gestures