Skip to content
This repository has been archived by the owner on Aug 29, 2024. It is now read-only.

Tutorial

Matt Green edited this page Jul 12, 2013 · 2 revisions

Introduction

Elevate takes a unique approach to dealing with asynchronous operations. Because it introduces a few new concepts, it can be helpful to discuss each of them at length.

First, let's examine a typical RubyMotion HTTP request using the excellent BubbleWrap library:

# Setup UI
SVProgressHUD.showWithStatus("Loading...")

# Initiate request
BW::HTTP.get(url) do |response|
  # Parse JSON
  items = BW::JSON.parse(response.body)

  # Store items in database
  Items.store(items)

  # Hide dialog
  SVProgressHUD.dismiss
end

This approach is recommended to start out with.

If requirements increase, however, it starts to creak a bit. Need to do something CPU intensive? You'll need to Dispatch::Queue it elsewhere. Have a second network request to make? You'll need to make another block. Do this enough, and you're headed for the callback pyramid of doom.

More importantly, the block-based approach imposes a cognitive overhead. Asynchronous operations are harder to compose, reason about, and test because they are asynchronous. Your core application logic runs the risk of being overwhelmed by (mostly) irrelevant details. It is easy to get it tangled up with the requirements surrounding threads and user interfaces. This muddles up the code, obscures the real intent, and makes it harder to iterate quickly.

Elevate can help. Let's review some of the key concepts.

Tasks

Elevate, at it's heart, is a DSL to declare tasks. A task is a single unit of work. It is akin to something you'd put in Sidekiq or Resque in a Rails app: it accepts input (if any), performs one or more actions, and, (perhaps) produces a value. It doesn't concern itself with the UI at all.

A task typically fulfills one user story. For example, in a social networking app, one task might be to register a new account. A possible user story for this is:

1. Get username and password
2. Submit credentials to web service
3. If registration succeeds, store them locally. Otherwise, inform the user.

Elevate's job is to make this story as clear as it possibly can. It does this by letting you write this story in a straightforward, sequential manner:

def registerClicked
  @register_task = async login: @login.text, password: @password.text do
    task do
      credentials = { login: @login, password: @password }
      response = Elevate::HTTP.post("https://example.com/users", form: credentials)

      if response.code == 201
        Credentials.store(response["user_id"], response["token"])

        true
      else
        false
      end
    end
  end
end

There are a few things going on here.

The async method launches a task, along with declaring it. It should be called from your view controller. It accepts an optional Hash of arguments to pass to the task block. Why don't we reference them from the outer block? his lets us workaround the infamous RM-3 bug, as well as safely pass data between the UI and background thread. It also makes it explicit what data the task needs to operate.

Next, there's the task block. It remains tightly focused on fulfilling the user story. It knows nothing about the surrounding user interface. task blocks run on a background thread, making them safe for expensive/slow operations. All data passed into async's argument Hash is made available as instance variables.

The return value of the task block is considered the result of the task. We'll get to that in a second.

IO Considerations

Elevate::HTTP, unlike every other iOS HTTP client, blocks the calling thread. This is by design. However, when used within Elevate's task blocks, Elevate::HTTP gains the ability for requests to be cancelled while in-flight.

Why was Elevate::HTTP designed this way? The primary motivation was to make network requests (a fundamentally asynchronous operation) feel synchronous, thus preserving Elevate's focus on simplifying your application logic. Thus, to maximize the benefits of Elevate, you'll need to use Elevate::HTTP for requests.

Cancellation

The async method returns an NSOperation representing your task. You may invoke cancel on it at anytime to interrupt execution. You need to cancel any tasks that might be running when your view controller is being torn down.

In the event of cancellation, Elevate::CancelledError is raised within your task block. The stack will begin unwinding from the current point of execution. You may use standard exception handling mechanisms (such as rescue and finally) to handle this case, but it shouldn't be necessary.

Technically, cancellation only works if execution is blocked by an Elevate-aware IO library, like Elevate::HTTP.

Callbacks

Tasks by themselves aren't very interesting: we need a way to interact with the UI at certain times. Elevate defines several callbacks that get fired during the lifetime of the task:

  • on_start - fired when your task is queued
  • on_update - fired whenever your task yields data
  • on_timeout - fired whenever your task's running time exceeds the timeout
  • on_finish - fired when your task has finished

Callbacks are meant to be used for updating the UI. Their declarative nature makes it clear how the UI responds to various changes in task state. They aren't well-suited to much more than that: all your application logic should be in the task block. Callbacks are always run on the UI thread.

All callbacks are optional, simply handle the ones of interest to you.

The on_finish callback deserves special mention: the first parameter of this block is called with the result of the task block. This is an excellent way to relay data back to the main thread.

Putting It All Together

Let's put together everything we've learned into a single annotated example:

@list_task = async do
  # Request a list of concerts from a web service
  task do
    Elevate::HTTP.get(File.join(ROOT, "index.json"))
  end

  # When we queue this task, display a modal dialog.
  on_start do
    SVProgressHUD.showWithStatus("Downloading...")
  end

  # When we finish this task, store the data, and hide the dialog.
  on_finish do |concerts, exception|
    SVProgressHUD.dismiss

    if exception == nil
      @concerts = concerts
      self.view.reloadData
    else
      UIAlertView.alloc.initWithTitle("Error", 
                                      message: exception.message,
                                      delegate: nil,
                                      cancelButtonTitle: nil,
                                      otherButtonTitles: "OK", nil).show
    end
  end
end

What's Next?

This is the end of the tutorial for Elevate. Feel free to poke around, play with it, and provide feedback!

Clone this wiki locally