Last active
August 29, 2015 14:25
-
-
Save zhigang1992/8ed2c3d26ddcffccd9b8 to your computer and use it in GitHub Desktop.
ReactiveCocoa RubyMotion Extension
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class UITextField | |
alias_method :text_signal, :rac_textSignal | |
end | |
class UIActionSheet | |
alias_method :button_signal, :rac_buttonClickedSignal | |
end | |
class UIControl | |
alias_method :event_signal, :rac_signalForControlEvents | |
end | |
class UIViewController | |
def rac_dismiss_vc(animated) | |
RACSignal.create! do |subscriber| | |
dismissViewControllerAnimated(animated, completion: ->() { | |
subscriber.sendCompleted | |
}) | |
RACDisposable.disposableWithBlock(nil) | |
end | |
end | |
def rac_present_vc(viewController, animated) | |
RACSignal.create! do |subscriber| | |
presentViewController(viewController, animated: animated, completion: ->() { | |
subscriber.sendCompleted | |
}) | |
RACDisposable.disposableWithBlock(nil) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class RACSignal | |
# RubyMotion's bridge support does not handle Objective-C methods that take block | |
# arguments typed as id so as to take blocks of varying arity. To work around this, | |
# an Objective-C category has been created with numbered methods, each explicitly | |
# typed, which pass the arguments to the original method. | |
# | |
# The same work-around will be required for all other methods that take an id block. | |
def self.reduceLatest(*signals, &block) | |
raise "Block must take #{signals.size} arguments to match the number of signals." if signals.size != block.arity | |
case block.arity | |
when 1 then | |
combineLatest(signals, reduce1: block) | |
when 2 then | |
combineLatest(signals, reduce2: block) | |
when 3 then | |
combineLatest(signals, reduce3: block) | |
when 4 then | |
combineLatest(signals, reduce4: block) | |
when 5 then | |
combineLatest(signals, reduce5: block) | |
end | |
end | |
# In RubyMotion, signals for boolean properties are resulting in values of 0 and 1, | |
# both of which evaluate as true in Ruby. Consequently the stream is full of true | |
# values. The work-around is to explicitly map the values to a boolean. | |
# See #to_bool defined below for TrueClass, FalseClass and Fixnum | |
def boolean | |
map ->(primitive) { primitive.to_bool } | |
end | |
# Create ! versions of a few ReactiveCocoa methods, allowing the methods to take | |
# a block the Ruby way and avoid explicit lambda expressions. | |
# This conflicts with the common semantics of using ! to imply the method modifies | |
# the receiver, but the alternatives (ex: map_, map?) are less appealing. | |
def map!(&block) | |
map(block) | |
end | |
def initially!(&block) | |
initially(block) | |
end | |
def finally!(&block) | |
finally(block) | |
end | |
def do_next!(&block) | |
doNext(block) | |
end | |
def do_error!(&block) | |
doError(block) | |
end | |
def do_completed!(&block) | |
doCompleted(block) | |
end | |
def do_all!(&block) | |
doNext(block) | |
.doError(block) | |
.doCompleted(block) | |
end | |
def filter!(&block) | |
filter(block) | |
end | |
def each!(&block) | |
subscribeNext(block) | |
end | |
alias_method :each, :each! | |
def each_error(&block) | |
subscribeError(block) | |
end | |
def throttle!(interval, &block) | |
return throttle(interval, valuesPassingTest: block) if block_given? | |
throttle(interval) | |
end | |
alias_method :error!, :each_error | |
def add_signal(&block) | |
addSignalBlock(block) | |
end | |
def reduce_next!(initial, &block) | |
combinePreviousWithStart(initial, reduce:block) | |
end | |
def reduce!(initial, &block) | |
scanWithStart(initial, combine: block) | |
end | |
alias_method :inject!, :reduce! | |
def first! | |
take(1) | |
end | |
def main_thread | |
deliverOn(RACScheduler.mainThreadScheduler) | |
end | |
def latest | |
switchToLatest | |
end | |
def aggregate!(start, &block) | |
aggregateWithStart(start, reduce: block) | |
end | |
def reduce_each!(&block) | |
map! do |args| | |
case block.arity | |
when 0 then | |
block.call() | |
when 1 then | |
block.call(args.first) | |
when 2 then | |
block.call(args.first, args.second) | |
when 3 then | |
block.call(args.first, args.second, args.third) | |
when 4 then | |
block.call(args.first, args.second, args.third, args.fourth) | |
when 5 then | |
block.call(args.first, args.second, args.third, args.fourth, args.fifth) | |
end | |
end | |
end | |
alias_method :distinct, :distinctUntilChanged | |
# Map a signal of truth values to a true value and false value. | |
def flip_flop(trueValue, falseValue) | |
map! do |truth| | |
truth ? trueValue : falseValue | |
end | |
end | |
class << self | |
def merge!(*signals) | |
self.merge(signals) | |
end | |
def combined!(*signals) | |
self.combineLatest(signals) | |
end | |
def concat!(*args) | |
self.concat(args) | |
end | |
def create!(&block) | |
self.createSignal(block) | |
end | |
def defer!(&block) | |
self.defer(block) | |
end | |
end | |
end | |
class RACStream | |
def named(name) | |
self.name = name | |
self | |
end | |
end | |
[TrueClass, FalseClass].each do |boolClass| | |
boolClass.class_exec do | |
def to_bool; | |
self | |
end | |
end | |
end | |
class Fixnum | |
def to_bool; | |
self != 0 | |
end | |
end | |
class RACMotionKeyPathAgent | |
def initialize(object, observer) | |
@object = object | |
@observer = observer | |
@keyPath = [] | |
end | |
def key(key) | |
@keyPath << key.to_s | |
self | |
end | |
def signal | |
@object.rac_signalForKeyPath(keyPath, observer: @observer) | |
end | |
def method_missing(method, *args, &block) | |
# Conclude when the method corresponds to a RACSignal method; see to RACAble() macro | |
if RACSignal.method_defined?(method) | |
signal.send(method, *args, &block) | |
# Conclude when assigning a signal; see RAC() macro | |
elsif args.size == 1 && args.first.is_a?(RACSignal) && method.to_s.end_with?('=') | |
key(method) | |
@object.rac_deriveProperty(keyPath.chop, from: args.first) | |
# Conclude when calling a non-RAC method with a signal argument, lift it | |
elsif !@keyPath.empty? && args.any? { |arg| arg.is_a?(RACSignal) } | |
# method_missing sets method to just the first argument's portion of the called selector. | |
# Construct the full selector. Obviously works only for Objective-C methods. | |
options = args.last.is_a?(Hash) ? args.pop : {} | |
selector = [method].concat(options.keys).join(':') << ':' | |
objects = args.concat(options.values) | |
target = @object.valueForKeyPath(keyPath) | |
if target.respondsToSelector(selector) | |
# RubyMotion can't splat an array into a varags paramter. Case it out. | |
o = objects | |
case objects.size | |
when 2 then | |
target.rac_liftSelector(selector, withObjects: o[0], o[1]) | |
when 3 then | |
target.rac_liftSelector(selector, withObjects: o[0], o[1], o[2]) | |
when 4 then | |
target.rac_liftSelector(selector, withObjects: o[0], o[1], o[3], o[4]) | |
end | |
else | |
raise "#{target.inspect} (via keyPath '#{keyPath}') does not respond to `#{selector}`" | |
end | |
# Continue when simple getter is called and extend the key path | |
else | |
key(method) | |
end | |
end | |
def keyPath | |
@keyPath.join('.') | |
end | |
alias_method :to_s, :keyPath | |
end | |
class RACTuple | |
def self.with_objects(*objects) | |
self.tupleWithObjectsFromArray(objects) | |
end | |
end | |
class Object | |
def signal_for_selector(selector) | |
rac_signalForSelector(selector) | |
end | |
def rac(object=self) | |
RACMotionKeyPathAgent.new(object, self) | |
end | |
# Capitalized doesn't work as well due to the required () to avoid it being treated as a constant | |
alias_method :RAC, :rac | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment