“ReactiveCocoa”:https://github.com/ReactiveCocoa/ReactiveCocoa is an “FRP”:http://en.wikipedia.org/wiki/Functional_reactive_programming framework written for Objective-C that makes it easy to observe, compose, and transform values. Recently, I had an opportunity to truly leverage the power of ReactiveCocoa when I found myself facing a complex user interface that took a multitude of inputs from the user.
h2. The Problem
The UI we were developing had the following features:
* A table of rows containing values that represent primary and optional secondary values.
* Each row’s primary and secondary values represent user inputs from a corresponding form.
* When the user submits the _composite_ form the associated form’s data must be merged.
Below is a rough sketch of the UI:
h2. The Toolbox
I tackled the above problem leveraging the following frameworks:
* “RubyMotion”:http://www.rubymotion.com – A toolchain that allows developers to create iOS applications using the Ruby language
* “ReactiveCocoa”:https://github.com/ReactiveCocoa/ReactiveCocoa
* “Formotion”:https://github.com/clayallsopp/formotion/ – A great library for creating iOS forms easily.
* “RubyMotion extensions”:https://github.com/kastiglione/RACSignupDemo-RubyMotion to ReactiveCocoa
h3. ReactiveCocoa’s Objective-C to Ruby Legend
For those of you that are familiar with Objective-C but not Ruby, you may find the legend below helpful.
* rac.property
is analogous to RACObserve(self, property)
* rac.property = signal
is analogous to RAC(self, property) = signal
h2. The Form
The center piece of the above mockup is the form (plus the user inputs and data returned by the form once it has been submitted). The first thing to do is track the form data as it changes, which is when the user submits the form.
class RACForm
attr_reader :form,
:data_signal
def initialize(form, primary_key, *secondary_keys)
@form = form
@data_signal = RACReplaySubject.replaySubjectWithCapacity RACReplaySubjectUnlimitedCapacity
self.data_signal.sendNext @form.render
@form.on_submit do |f|
self.data_signal.sendNext @form.render
end
end
end
The RACForm
wraps the Formotion::Form
and creates a signal that can be used to observe changes to the form data.
h3. Why not just subscribe to Form#on_submit?
The advantage to creating a signal over listening to an event is two fold:
# A signal can be mapped, filtered, reduced, etc.
# The on_submit
event is limited to only one subscriber.
h2. Primary Value
Each row in the table has a primary value that represents _a_ data input in a form. Now that the data changes are being tracked the signal can be mapped to return the _primary value_ of a form.
class RACForm
attr_reader :form,
:data_signal,
:primary_value
def initialize(form, primary_key, *secondary_keys)
@form = form
@data_signal = RACReplaySubject.replaySubjectWithCapacity RACReplaySubjectUnlimitedCapacity
@primary_value = @data_signal.map! do |data|
lookup_title(@form, data, primary_key)
end.replayLast
self.data_signal.sendNext @form.render
@form.on_submit do |f|
self.data_signal.sendNext @form.render
end
end
private
def lookup_title(form, data, key)
row = form.row(key) || form.row(data[key])
if row
case row.type
when :subform
subform = row.subform.to_form
return lookup_title(subform, subform.render, key)
when :check
return row.title
else
return row.value
end
end
data[key]
end
end
Now, each time the form is submitted the _data signal_ is mapped to a single _primary value_ signal which can be used by the table row to display it.
class UITableViewCell
def initWithForm(form)
self.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier: "IDENTIFIER") do
rac.textLabel.text = form.primary_value
end
end
end
Look how easy that was! The text of the cell is automatically updated anytime the form changes.
h2. Secondary Values
A form could very well contain a multitude of user inputs where there is a _primary value_, and a few _secondary values_ that should be displayed just below the primary value.
Creating a secondary values signal isn’t all that different from creating the primary value signal. The only difference is there is a collection of secondary values and only one primary value.
def initialize(form, primary_key, *secondary_keys)
...
@secondary_values = @data_signal.map! do |data|
secondary_keys.map { |k| lookup_title(@form, data, k) }
end.replayLast
...
end
Updating the cell to support secondary values is fairly easy at this point.
class UITableViewCell
def initWithForm(form)
self.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier: "IDENTIFIER") do
rac.textLabel.text = form.primary_value
rac.detailTextLabel.text = form.secondary_values.map! do |x|
x.join(", ")
end
end
end
end
The cell now only has to map the secondary_values
signal into a single value joined by a comma. The subtitle will automatically be updated when the form is updated.
h2. Multiple Forms
After each form is completed the table can be submitted where the data in each form is merged and can be used to display the next screen or submit the data to an API, for example.
ReactiveCocoa comes with a nifty way to track multiple signals via RACSignal.combineLatest
.
class FormViewController
def initWithForms(forms)
super.initWithNibName(nil, bundle: nil) do
@forms = forms
@composite_signal = RACSignal.combineLatest(@forms.map { |f| f.data_signal.startWith(f.form.render) }).map! do |tuple|
combined = {}
tuple.allObjects.each do |x|
combined.merge! flatten_values(x)
end
combined
end
end
end
private
def flatten_values(x)
hash = {}
x.each do |k,v|
if v.is_a? Hash
hash.merge! flatten_values(v)
else
hash.merge!({k => v})
end
end
hash
end
end
The controller creates a composite signal composed of each form’s #data_signal
. Now, when the user is updating each form, the composite signal is automatically updated with composite dataset which can be used when the user submits the table.
h2. Conclusion
Creating a complex interactive UI is a difficult and a potentially error prone development activity — full of state and tons of boiler plate code. However, FRP frameworks like ReactiveCocoa make it significantly easier to create and manage complex user interfaces. Moreover, refactoring, updating, or adding to the UI become subsequently easier.