README.md in elevate-0.6.0 vs README.md in elevate-0.7.0

- old
+ new

@@ -1,162 +1,116 @@ Elevate ====== -Stop scattering your domain logic across your view controller. Consolidate it to a single conceptual unit with Elevate. +Are you suffering from symptoms of Massive View Controller? +Use Elevate to elegantly decompose tasks, and give your view controller a break. + [![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate) [![Travis](https://api.travis-ci.org/mattgreen/elevate.png)](https://travis-ci.org/mattgreen/elevate) [![Gem Version](https://badge.fury.io/rb/elevate.png)](http://badge.fury.io/rb/elevate) -Example -------- -```ruby -@login_task = async username: username.text, password: password.text do - # This block runs on a background thread. - task do - # @username and @password correspond to the Hash keys provided to async. - args = { username: @username, password: @password } +Introduction +------------ +Your poor, poor view controllers. They do **so** much for you, and they get rewarded with *even more* responsibilities: - credentials = Elevate::HTTP.post(LOGIN_URL, args) - if credentials - UserRegistration.store(credentials.username, credentials.token) - end +* Handle user input +* Update the UI in response to that input +* Request data from web services +* Load/save/query your model layer - # Anything yielded from this block is passed to on_update - yield "Logged in!" +In reality, view controllers attract complexity because they often act as a conceptual junk drawer of glue code. Fat controllers are major anti-pattern in Rails, yet iOS controllers are tasked with even more concerns. - # Return value of block is passed back to on_finish - credentials != nil - end +Elevate is your view controller's best friend, shouldering some of these burdens. It cleanly separates the unique behavior of your view controller (that is, what it is actually *meant* to do) from the above concerns, letting your view controller breathe more. Ultimately, Elevate makes your view controllers easier to understand and modify. - on_start do - # This block runs on the UI thread after the operation has been queued. - SVProgressHUD.showWithStatus("Logging In...") - end +This is a rather bold claim. Let's look at an example: - on_update do |status| - # This block runs on the UI thread with anything the task yields - puts status +```ruby +class ArtistsViewController < UIViewController + include Elevate + + def viewDidLoad + super + + launch(:update) end - on_finish do |result, exception| - # This block runs on the UI thread after the task block has finished. - SVProgressHUD.dismiss + task :update do + background do + response = Elevate::HTTP.get("https://example.org/artists") + DB.update(response) - if exception == nil - if result - alert("Logged in successfully!") - else - alert("Invalid username/password!") - end - else - alert(exception) + response end + + on_start do + SVProgressHUD.showWithStatus("Loading...") + end + + on_finish do |response, exception| + SVProgressHUD.dismiss + + self.artists = response + end end end ``` -Background ------------ -Many iOS/OS X apps have fairly simple domain logic that is obscured by several programming 'taxes': +We define a task named `update`. Within that task, we specify the work that it does using the `background` method of the DSL. As the name implies, this block runs on a background thread. Next, we specify two callback handlers: `on_start` and `on_finish`. These are run when the task starts and finishes, respectively. Because these are alwys run on the main thread, you can use them to update the UI. Finally, in `viewDidLoad`, we start the task by calling `launch`, passing the name of the task. -* UI management -* asynchronous network requests -* I/O-heavy operations, such as storing large datasets to disk +Notice that it is very clear what the actual work of the `update` task is: getting a list of artists from a web service, storing the results in a database, and passing the list of artists back. Thus, you should view Elevate as a DSL for a *very* common pattern for view controllers: -These are necessary to ensure a good user experience, but they splinter your domain logic (that is, what your application does) through your view controller. Gross. +1. Update the UI, telling the user you're starting some work +2. Do the work (possibly storing it in a database) +3. Update the UI again in response to what happened -Elevate is a mini task queue for your app, much like Resque or Sidekiq. Rather than defining part of an operation to run on the UI thread, and a CPU-intensive portion on a background thread, Elevate is designed so you run the *entire* operation in the background, and receive notifications at various times. This has a nice side effect of consolidating all the interaction for a particular task to one place. The UI code is cleanly isolated from the non-UI code. When your tasks become complex, you can elect to extract them out to a service object. +(Some tasks may not need steps 1 or 3, of course.) -In a sense, Elevate is almost a control-flow library: it bends the rules of app development a bit to ensure that the unique value your application provides is as clear as possible. +Taming Complexity +-------- +You may have thought that Elevate seemed a bit heavy for the example code. I'd agree with you. +Elevate was actually designed to handle the more complex interactions within a view controller: + +* **Async HTTP**: Elevate's HTTP client blocks, letting you write simple, testable I/O. Multiple HTTP requests do not suffer from the Pyramid of Doom effect, allowing you to easily understand dataflow. It also benefits from... +* **Cancellation**: tasks may be aborted while they are running. (Any in-progress HTTP request is aborted.) +* **Errors**: exceptions raised in a `background` block are reported to a callback, much like `on_finish`. Specific callbacks may be defined to handle specific exceptions. +* **Timeouts**: tasks may be defined to only run for a maximum amount of time, after which they are aborted, and a callback is invoked. + +The key point here is that defining a DSL for tasks enables us to **abstract away tedious and error-prone** functionality that is common to many view controllers, and necessary for a great user experience. Why re-write this code over and over? + Documentation -------- +To learn more: + - [Tutorial](https://github.com/mattgreen/elevate/wiki/Tutorial) - start here - [Wiki](https://github.com/mattgreen/elevate/wiki) +Requirements +------------ + +- iOS 5 and up or OS X + +- RubyMotion 2.x - Elevate pushes the limits of RubyMotion. Please ensure you have the latest version before reporting bugs! + Installation ------------ Update your Gemfile: - gem "elevate", "~> 0.6.0" + gem "elevate", "~> 0.7.0" Bundle: $ bundle install -Usage ------ - -Include the module in your view controller: - -```ruby -class ArtistsSearchViewController < UIViewController - include Elevate -``` - -Launch an async task with the `async` method: -```ruby -@track_task = async artist: searchBar.text do - task do - response = Elevate::HTTP.get("http://example.com/artists", query: { artist: @artist }) - - artist = Artist.from_hash(response) - ArtistDB.update(artist) - - response["name"] - end - - on_start do - SVProgressHUD.showWithStatus("Adding...") - end - - on_finish do |result, exception| - SVProgressHUD.dismiss - end -end -``` - -If you might need to cancel the task later, call `cancel` on the object returned by `async`: -```ruby -@track_task.cancel -``` - -Timeouts --------- -Elevate 0.6.0 includes support for timeouts. Timeouts are declared using the `timeout` method within the `async` block. They start when an operation is queued, and automatically abort the task when the duration passes. If the task takes longer than the specified duration, the `on_timeout` callback is run. - -Example: - -```ruby -async do - timeout 0.1 - - task do - Elevate::HTTP.get("http://example.com/") - end - - on_timeout do - puts 'timed out' - end - - on_finish do |result, exception| - puts 'completed!' - end -end - - -``` - -Caveats ---------- -* Must use Elevate's HTTP client instead of other iOS networking libs - Inspiration ----------- +This method of organizing work recurs on several platforms, due to its effectiveness. I've stolen many ideas from these sources: + * [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture) -* [Android SDK's AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html) -* Go (asynchronous IO done correctly) +* Android: [AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html) +* .NET: BackgroundWorker +* Go's goroutines for simplifying asynchronous I/O License --------- MIT License