-
Notifications
You must be signed in to change notification settings - Fork 10
Tutorial
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.
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.
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.
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
.
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.
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
This is the end of the tutorial for Elevate. Feel free to poke around, play with it, and provide feedback!