RubyMotion promises to bring the clarity and concise syntax of Ruby to iOS and OS X development. ReactiveCocoa’s aim is to help reduce complexity by deriving state instead of declaring it. Sounds like a great combination, right?
There are some complications. First, the documentation and examples almost all use the RAC and RACAble macros provided by ReactiveCocoa, something we can’t use from RubyMotion. Bummer. And RubyMotion’s bridgesupport doesn’t allow us to call Objective-C methods that take blocks typed as id. Rats. Fortunately these can be overcome, thanks to some helpful folks on the internet, so let’s get started.
1. Install ReactiveCocoa
ReactiveCocoa is available through CocoaPods. Update your RubyMotion project’s Rakefile to include it:
Motion::Project::App.setup do |app|
...
app.pods do
pod 'ReactiveCocoa'
end
...
end
The next time you build your project, CocoaPods will retrieve and build the ReactiveCocoa library. There are RAC extensions for many popular libraries. I recommend checking out “AFNetworking-RACExtensions”:https://github.com/CodaFi/AFNetworking-RACExtensions in particular.
2. Set Up a Shim for RubyMotion
We ran across “an example project from Dave Lee”:https://github.com/kastiglione/RACSignupDemo-RubyMotion. He sets up a convenient shim that solves both of the complications I mentioned in the opening. There are two primary pieces.
On “line 84 of IMMViewController.rb”:https://github.com/kastiglione/RACSignupDemo-RubyMotion/blob/master/app/IMMViewController.rb#L84 a new class method, @reduceLatest@, is added to the RACSignal class. This method wraps the existing @combineLatest@ to resolve the problem of passing blocks as type @id@. It works in conjunction with an “Objective-C shim and .bridgesupport file”:https://github.com/kastiglione/RACSignupDemo-RubyMotion/tree/master/vendor/ReactiveMotion that enumerate methods taking blocks of specific arity (1-5 in this case, add more if you need them).
The other piece, “starting at line 159 of IMMViewController.rb”:https://github.com/kastiglione/RACSignupDemo-RubyMotion/blob/master/app/IMMViewController.rb#L159, is a replacement for the convenient @RAC@ and @RACAble@ macros. Keep in mind that objects passed to the @rac@ method need to be KVO-compliant.
Overall, lines 77 – 219 of IMMViewController.rb and the contents of the vendor/ReactiveMotion make a pretty good shim.
3. Read the Docs
ReactiveCocoa’s paradigms can be challenging to get a good grasp on. Fortunately the team has provided “good documentation”:https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation. Read it. In particular, I’d recommend starting with the Readme and the Framework Overview.
Justin DeWind has a nice collection of resources in his post, “ReactiveCocoa: The Future of Cocoa Programming”:https:/reactivecocoa/
I’ve also had a great experience diving into ReactiveCocoa’s source code.
Example: Triggering Authentication Using a Command
This is an example of using a command to start the authentication process.
class LoginViewController < UIViewController
attr_accessor :credentials
def viewDidLoad
super
@credentials = RACSignal.reduceLatest(usernameField.rac_textSignal, passwordField.rac_textSignal) do |user, pass|
@_credentials = [user, pass]
end
end
# Wired up to a button in our storyboard
def signInClicked
@command.execute(@_credentials)
end
end
# elsewhere, in a class that has a reference to the
# LoginViewController (@view) and user model (@user)
class LoginManager
attr_accessor :credentialsAreNotEmpty
...
# Gets triggered after the view for the view controller above is loaded
def configure
@credentialsAreNotEmpty = @view.credentials.map -> (creds) do
creds.length == 2 and !creds.any? {|c| c.nil? or c.empty? }
end
@authCommand = RACCommand.commandWithCanExecuteSignal credentialsAreNotEmpty
@authCommand.allowsConcurrentExecution = false
# The command's `executing` will remain YES until the
# signal returned by the block completes or errors.
@authCommand.addSignalBlock ->(credentials) do
# User#authenticate uses AFNetworking-RACExtensions under the hood and
# returns a signal that completes when the auth request is complete.
@user.authenticate credentials
end
end
end
One thing I’d love to find a better way to accomplish is aggregating the command execution with the stream of current credentials. Keeping the added state @_credentials is gross. I tried a few variations of reduceLatest but didn’t get the behavior I was looking for.
Great post, RubyMotion and ReactiveCocoa are both awesome.
Glad you were able to get some use out of that demo code, but I must point out I was very new to RAC at the time and looking at it now, a bunch of it is not so good.
I’m putting together the beginnings of an actual gem that uses the good parts of that demo code: https://github.com/kastiglione/rmrac
The idea is that will be a lower level gem, which can then be built on to do Ruby DSL’s etc.
Also, for getting rid of the @_credentials state, checkout RACSignal#executeCommand
@credentials = RACSignal.reduceLatest(usernameField.rac_textSignal, passwordField.rac_textSignal) do |user, pass|
[user, pass]
end
@credentials.executeCommand(@authCommand)