A React.js Pattern for your RubyMotion app

Post summary: I build a "Bitcoin price" RubyMotion iOS app and show how it can be made to follow more of a React.js pattern.


At ClearSight we are venturing beyond Rails and into the realm of React.js and Ember.js client-side JavaScript frameworks. The experience has been both painful and enlightening. Most worthwhile learning experiences are a bit of both.

In this post, I'm going to show how you normally build an app. All of the usual actions (initialization, asynchronous data load, user touches) will affect the view in some way, and the cognitive load will increase. Then I'll refactor using the lessons learned from React.js and show how the cognitive load is reduced drastically.

For this discussion, let's say that we're building an app that retrieves the current Bitcoin price and converts it to various currencies.

Building an app the normal way

We're going to use RedPotion (currently 1.0.0), which is ProMotion and RMQ and several other useful gems pulled together into one.

Start off by generating the app:

$ gem install redpotion
$ potion create bitcoin-app
$ cd bitcoin-app
$ bundle
$ rake pod:install

Build the screen

Generate a new screen using RedPotion's generator:

$ potion create screen bit_coin

This will create a screen file, a stylesheet, and a spec file. We're going to skip the spec file for now, but we will definitely revisit it when we make this more like React.js.

Let's build a very simplified UI using RedPotion's RMQ append command.

# app/screens/bit_coin_screen.rb
class BitCoinScreen < PM::Screen  
  title "Bitcoin Price"
  stylesheet BitCoinScreenStylesheet

  def on_load
    append(UILabel, :bitcoin_price).data("Loading...")
    append(UILabel, :last_fetched_date).data("...")
  end
end  

Update your stylesheet

This will be a very simple stylesheet.

# app/stylesheets/bit_coin_screen_stylesheet.rb
class BitCoinScreenStylesheet < ApplicationStylesheet  
  def root_view(st)
    st.background_color = color.white
  end

  def bitcoin_price(st)
    st.frame = {
      left:           20,
      from_right:     20,
      top:            100,
      height:         50,
    }
    st.text_alignment = :center
    st.font = UIFont.boldSystemFontOfSize(40.0)
  end

  def last_fetched_date(st)
    st.frame = {
      left:           20,
      from_right:     20,
      below_previous: 100,
      height:         50,
    }
    st.text_alignment = :center
    st.font = UIFont.systemFontOfSize(24.0)
  end
end  

Update the App Delegate

Since this is a single-screen app, your AppDelegate will be very tiny:

class AppDelegate < PM::Delegate  
  def on_load(app, options)
    return true if RUBYMOTION_ENV == "test"
    open BitCoinScreen
  end
end  

Run the app

Run rake and you should get an app that looks like this:

simulator screenshot

(Note: if you get "ERROR! Can't locate iPhoneSimulator SDK 8.1" then just go delete the app.sdk_version line in the Rakefile.)

Add BTC currency fetching

We already have AFMotion enabled in our Gemfile (add it if it's not there with gem "AFMotion"). So let's fetch the data using the BitcoinAverage Price Index API. In your screen:

# app/screens/bit_coin_screen.rb
class BitCoinScreen < PM::Screen  
  title "Bitcoin Price"
  stylesheet BitCoinScreenStylesheet

  def on_load
    @bitcoin_prices = {}
    @bitcoin_price = nil
    @last_fetched = nil
    @currency = "USD"

    append(UILabel, :bitcoin_price).data("Loading...")
    append(UILabel, :last_fetched_date).data("...")

    load_prices
  end

  def load_prices
    AFMotion::JSON.get("https://api.bitcoinaverage.com/all") do |result|
      if result.success?
        @bitcoin_prices = result.object
        @bitcoin_price = @bitcoin_prices[@currency]["global_averages"]["last"]
        find(:bitcoin_price).data("#{@bitcoin_price} #{@currency}")
        find(:last_fetched_date).data(Time.now.strftime("%b %e, %l:%M %p"))
      else
        mp result
      end
    end
  end

end  

This time, when we run rake we get some data!

simulator with data

Now let's add a button that changes the currency every time you press it. Easy enough, just add this to your on_load:

# app/screens/bit_coin_screen.rb
class BitCoinScreen < PM::Screen  
  title "Bitcoin Price"
  stylesheet BitCoinScreenStylesheet

  def on_load
    @bitcoin_prices = {}
    @bitcoin_price = nil
    @last_fetched = nil
    @currency = "USD"
    @currencies = [ "USD", "AUD", "CAD", "EUR" ]

    append(UILabel, :bitcoin_price).data("Loading...")
    append(UILabel, :last_fetched_date).data("...")
    append(UIButton, :cycle_currency).data("USD").on(:tap) do
      # Get next currency
      @currency = @currencies.rotate(@currencies.index(@currency) + 1).first
      @bitcoin_price = @bitcoin_prices[@currency]["global_averages"]["last"]
      find(:bitcoin_price).data("#{@bitcoin_price} #{@currency}")
    end

    load_prices
  end

  # ... omitted
end  

Update your stylesheet to set the frame for this new button:

# app/stylesheets/bit_coin_screen_stylesheet.rb
class BitCoinScreenStylesheet < ApplicationStylesheet

  # ... omitted

  def cycle_currency(st)
    st.frame = {
      left:           20,
      from_right:     20,
      below_previous: 100,
      height:         50,
    }
    st.font = UIFont.systemFontOfSize(24.0)
    st.color = UIColor.blackColor
  end
end  

When the user taps the button, it'll cycle through a few currencies and display their prices.

Run it and tap the button.

simulator with button

Here's a link to the code up to this point:

https://github.com/jamonholmgren/bitcoin-app/tree/normal

Refactoring to a React.js pattern

The app works fine, but there are several places that interact with the UI when something happens to reflect the changes.

state changes

One of the concepts that I keep coming back to is the idea of the UI being a function of application state; in other words, given a particular set of data, your UI should always look the same. This is one of the fundamental concepts taken from React.js. They refer to this as "one-way data flow".

Let's take a look at what happens when we apply the principle that the UI should reflect the current application state.

What would our app state look like? Let's use a simple hash, updating our on_load method:

  def on_load
    @state = {
      bitcoin_prices: {},
      last_fetched: nil,
      currency: "USD",
      currencies: [ "USD", "AUD", "CAD", "EUR" ],
    }
    set_state(@state)
    load_prices
  end

The line before load_prices is a new method call, set_state. Let's build it:

  def set_state(state)
    # Build the UI initially if it hasn't been built yet
    build_initial_ui if find(:bitcoin_price).length == 0

    bitcoin_price = bitcoin_price_for_currency(state)

    # Set all the UI elements to reflect the current state
    find(:bitcoin_price).data("#{bitcoin_price} #{state[:currency]}")
    find(:last_fetched_date).data(state[:last_fetched_date])
    find(:cycle_currency).data(state[:currency])
  end

  def build_initial_ui
    append(UILabel, :bitcoin_price)
    append(UILabel, :last_fetched_date)
    append(UIButton, :cycle_currency).on(:tap) do
      rotate_currency
    end
  end

  def rotate_currency
    @state[:currency] = @state[:currencies].rotate(@state[:currencies].index(@state[:currency]) + 1).first
    set_state(@state)
  end

  def bitcoin_price_for_currency(state)
    return "Loading" unless state[:bitcoin_prices][state[:currency]]
    state[:bitcoin_prices][state[:currency]]["global_averages"]["last"]
  end

There's a lot of familiar code. It checks to see if the views have been built yet and builds them if not. Then, it sets all the state data (like current bitcoin price, currency, button title). If the button is tapped, it calls rotate_currency which just rotates the currency and calls set_state again to update the UI.

The load_prices method gets simpler. Check this out:

  def load_prices
    AFMotion::JSON.get("https://api.bitcoinaverage.com/all") do |result|
      if result.success?
        @state[:bitcoin_prices] = result.object
        @state[:last_fetched_date] = Time.now.strftime("%b %e, %l:%M %p")
        set_state(@state)
      else
        mp result
      end
    end
  end

There's only one method that manipulates the UI -- set_state. It can take any state hash that has the proper structure, so the screen just manipulates the @state hash and calls set_state(@state) anytime something has changed.

Run it and you'll find that the app works just like before.

Here's a link to the diff:

https://github.com/jamonholmgren/bitcoin-app/commit/fcd635384e4b8e24425faeec5fd3cbbbd3a47367

And the branch:

https://github.com/jamonholmgren/bitcoin-app/tree/react

What's the benefit?

It's about the same amount of code. I'm sure we could refactor the original to bring it in line with the React-like version. So, what benefits do we gain from this approach?

The main benefit is cognitive. The only way the UI ever gets updated is with set_state, so if you have a UI element that isn't updating properly, you know where to look -- either in set_state or in something manipulating @state and calling set_state.

react version

Another benefit is that the app state is very encapsulated, in one hash. You could serialize that when you exit the app and then unserialize. When you call set_state with the unserialized data, you'll be right back where you left off. You could even implement an undo feature in not too many lines of code or track history.

You can also provide UI abstraction much easier, such as iPhone and iPad UI versions, or even iOS and Android. Look at this:

def set_state(state)  
  update_ui_android(state) if android?
  update_ui_iphone(state) if ios? && iphone?
  update_ui_ipad(state) if ios? && ipad?
end  

Consider how complex a UI update would be in a cross-platform app without this single point of abstraction.

And lastly, you can test the UI so much easier. Let's write a test.

Testing

With the first example, there would need to be a lot of mocking or something like instance_variable_set to put the screen into a state that could be tested. With this, you can easily test the UI.

Go into your spec folder and edit the bit_coin_screen_spec.rb file like this:

# spec/screens/bit_coin_screen_spec.rb
describe 'BitCoinScreen' do  
  it "sets the current bitcoin price in USD" do
    screen = BitCoinScreen.new
    screen.set_state({
      bitcoin_prices: {
        "USD" => { "global_averages" => { "last" => 2.52 } },
      },
      last_fetched_date: "2015-03-21",
      currency: "USD",
      currencies: [ "USD" ],
    })

    screen.find(:bitcoin_price).data.should == "2.52 USD"
    screen.find(:last_fetched_date).data.should == "2015-03-21"
    screen.find(:cycle_currency).data.should == "USD"
  end
end  

Run that spec. It should pass:

BitCoinScreen  
  - sets the current bitcoin price in USD

1 specifications (3 requirements), 0 failures, 0 errors  

It's a fast and effective way to test UI elements. We aren't even mounting the screen in the simulator, but rather just testing it in memory. The speed gains are pretty impressive.

Looking forward

This is just the beginning. The RedPotion team is talking about how to implement a more sophisticated and idiomatic version of this in future versions of RedPotion. Perhaps a render method in your layout and some sort of view hierarchy diffing mechanism would make sense.

For ClearSight, we find that the set_state method is where our front end engineers and back end engineers meet. The front end can mock up the data they need to build the UI and call set_state, and the back end can hook up the real data and call set_state without worrying about what happens after that all that much. It's a great way to meet in the middle.

If you're interested in being involved in the discussion, I've opened a Github issue on RedPotion. Or let me know what you think on Twitter!


Hat tip to @hackflow for the great article, Boiling React down to a few lines of jQuery, which provided the inspiration for this blog post. Also thanks to Matt Green, Darin Wilson, and Matthew Sinclair for their feedback on early drafts.