elevate 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 960a321bb02005b2ff94c974ae37afbd9905b027
4
- data.tar.gz: c5a04645e4e674e4d7285ab4c705a8b52c0561e2
3
+ metadata.gz: a2fa20b0d90cf439fac88fe64dc15375b1ba2dba
4
+ data.tar.gz: 0c1df21032e6d395b64290813029b00fe8b00f9a
5
5
  SHA512:
6
- metadata.gz: f56b43560ddf1be5e5c1cd9cc940bf15cc44719442e5d28fb7ff7022958f67e699245ef4698e6f1ec34210f00ede2e7e57f01f07d9ec144ef8f3eab1d51af700
7
- data.tar.gz: 64a8d83ae84ec87a4daf9f78c40f220b1763bc15a6d6370e3ff142b9619acf6e718571e033085a2675c175e031f0ddee2f1d962b40149e2c0c51a11e3410ab6f
6
+ metadata.gz: c5d22a4680de13370bc42892bb5e29c004a4ea26e0c60479a2b83e804d24d8d765e3f7f36f896ae62c8e4affcde95e001838736b0afc1581b8c83cb8a8fe0390
7
+ data.tar.gz: 77d606888fc5bc2d7f35586ee4d5d302bc0d681aa1094a9470426ba86da5000b066e90b75113094f94fe77142956a7281ee7121497d72de81edc8fb448036be1
data/README.md CHANGED
@@ -1,161 +1,115 @@
1
1
  Elevate
2
2
  ======
3
3
 
4
- Stop scattering your domain logic across your view controller. Consolidate it to a single conceptual unit with Elevate.
4
+ Are you suffering from symptoms of Massive View Controller?
5
+
6
+ Use Elevate to elegantly decompose tasks, and give your view controller a break.
5
7
 
6
8
  [![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)
7
9
 
8
- Example
9
- -------
10
+
11
+ Introduction
12
+ ------------
13
+ Your poor, poor view controllers. They do **so** much for you, and they get rewarded with *even more* responsibilities:
14
+
15
+ * Handle user input
16
+ * Update the UI in response to that input
17
+ * Request data from web services
18
+ * Load/save/query your model layer
19
+
20
+ 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.
21
+
22
+ 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.
23
+
24
+ This is a rather bold claim. Let's look at an example:
10
25
 
11
26
  ```ruby
12
- @login_task = async username: username.text, password: password.text do
13
- # This block runs on a background thread.
14
- task do
15
- # @username and @password correspond to the Hash keys provided to async.
16
- args = { username: @username, password: @password }
17
-
18
- credentials = Elevate::HTTP.post(LOGIN_URL, args)
19
- if credentials
20
- UserRegistration.store(credentials.username, credentials.token)
21
- end
27
+ class ArtistsViewController < UIViewController
28
+ include Elevate
22
29
 
23
- # Anything yielded from this block is passed to on_update
24
- yield "Logged in!"
30
+ def viewDidLoad
31
+ super
25
32
 
26
- # Return value of block is passed back to on_finish
27
- credentials != nil
33
+ launch(:update)
28
34
  end
29
35
 
30
- on_start do
31
- # This block runs on the UI thread after the operation has been queued.
32
- SVProgressHUD.showWithStatus("Logging In...")
33
- end
36
+ task :update do
37
+ background do
38
+ response = Elevate::HTTP.get("https://example.org/artists")
39
+ DB.update(response)
34
40
 
35
- on_update do |status|
36
- # This block runs on the UI thread with anything the task yields
37
- puts status
38
- end
41
+ response
42
+ end
39
43
 
40
- on_finish do |result, exception|
41
- # This block runs on the UI thread after the task block has finished.
42
- SVProgressHUD.dismiss
43
-
44
- if exception == nil
45
- if result
46
- alert("Logged in successfully!")
47
- else
48
- alert("Invalid username/password!")
49
- end
50
- else
51
- alert(exception)
44
+ on_start do
45
+ SVProgressHUD.showWithStatus("Loading...")
46
+ end
47
+
48
+ on_finish do |response, exception|
49
+ SVProgressHUD.dismiss
50
+
51
+ self.artists = response
52
52
  end
53
53
  end
54
54
  end
55
55
  ```
56
56
 
57
- Background
58
- -----------
59
- Many iOS/OS X apps have fairly simple domain logic that is obscured by several programming 'taxes':
57
+ 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.
60
58
 
61
- * UI management
62
- * asynchronous network requests
63
- * I/O-heavy operations, such as storing large datasets to disk
59
+ 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:
64
60
 
65
- 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.
61
+ 1. Update the UI, telling the user you're starting some work
62
+ 2. Do the work (possibly storing it in a database)
63
+ 3. Update the UI again in response to what happened
66
64
 
67
- 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.
65
+ (Some tasks may not need steps 1 or 3, of course.)
68
66
 
69
- 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.
67
+ Taming Complexity
68
+ --------
69
+ You may have thought that Elevate seemed a bit heavy for the example code. I'd agree with you.
70
+
71
+ Elevate was actually designed to handle the more complex interactions within a view controller:
72
+
73
+ * **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...
74
+ * **Cancellation**: tasks may be aborted while they are running. (Any in-progress HTTP request is aborted.)
75
+ * **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.
76
+ * **Timeouts**: tasks may be defined to only run for a maximum amount of time, after which they are aborted, and a callback is invoked.
77
+
78
+ 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?
70
79
 
71
80
  Documentation
72
81
  --------
82
+ To learn more:
83
+
73
84
  - [Tutorial](https://github.com/mattgreen/elevate/wiki/Tutorial) - start here
74
85
 
75
86
  - [Wiki](https://github.com/mattgreen/elevate/wiki)
76
87
 
88
+ Requirements
89
+ ------------
90
+
91
+ - iOS 5 and up or OS X
92
+
93
+ - RubyMotion 2.x - Elevate pushes the limits of RubyMotion. Please ensure you have the latest version before reporting bugs!
94
+
77
95
  Installation
78
96
  ------------
79
97
  Update your Gemfile:
80
98
 
81
- gem "elevate", "~> 0.6.0"
99
+ gem "elevate", "~> 0.7.0"
82
100
 
83
101
  Bundle:
84
102
 
85
103
  $ bundle install
86
104
 
87
- Usage
88
- -----
89
-
90
- Include the module in your view controller:
91
-
92
- ```ruby
93
- class ArtistsSearchViewController < UIViewController
94
- include Elevate
95
- ```
96
-
97
- Launch an async task with the `async` method:
98
- ```ruby
99
- @track_task = async artist: searchBar.text do
100
- task do
101
- response = Elevate::HTTP.get("http://example.com/artists", query: { artist: @artist })
102
-
103
- artist = Artist.from_hash(response)
104
- ArtistDB.update(artist)
105
-
106
- response["name"]
107
- end
108
-
109
- on_start do
110
- SVProgressHUD.showWithStatus("Adding...")
111
- end
112
-
113
- on_finish do |result, exception|
114
- SVProgressHUD.dismiss
115
- end
116
- end
117
- ```
118
-
119
- If you might need to cancel the task later, call `cancel` on the object returned by `async`:
120
- ```ruby
121
- @track_task.cancel
122
- ```
123
-
124
- Timeouts
125
- --------
126
- 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.
127
-
128
- Example:
129
-
130
- ```ruby
131
- async do
132
- timeout 0.1
133
-
134
- task do
135
- Elevate::HTTP.get("http://example.com/")
136
- end
137
-
138
- on_timeout do
139
- puts 'timed out'
140
- end
141
-
142
- on_finish do |result, exception|
143
- puts 'completed!'
144
- end
145
- end
146
-
147
-
148
- ```
149
-
150
- Caveats
151
- ---------
152
- * Must use Elevate's HTTP client instead of other iOS networking libs
153
-
154
105
  Inspiration
155
106
  -----------
107
+ This method of organizing work recurs on several platforms, due to its effectiveness. I've stolen many ideas from these sources:
108
+
156
109
  * [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture)
157
- * [Android SDK's AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)
158
- * Go (asynchronous IO done correctly)
110
+ * Android: [AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)
111
+ * .NET: BackgroundWorker
112
+ * Go's goroutines for simplifying asynchronous I/O
159
113
 
160
114
  License
161
115
  ---------
@@ -12,8 +12,9 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
13
  gem.name = "elevate"
14
14
  gem.require_paths = ["lib"]
15
+ gem.license = 'MIT'
15
16
  gem.version = Elevate::VERSION
16
17
 
17
- gem.add_development_dependency 'rake', '>= 0.9.0'
18
+ gem.add_development_dependency 'rake'
18
19
  gem.add_development_dependency 'webstub', '~> 0.6.0'
19
20
  end
@@ -0,0 +1,19 @@
1
+ module Elevate
2
+ # A simple unidirectional stream of data with a single consumer.
3
+ #
4
+ # @api private
5
+ class Channel
6
+ def initialize(block)
7
+ @target = block
8
+ end
9
+
10
+ # Pushes data to consumers immediately
11
+ #
12
+ # @return [void]
13
+ #
14
+ # @api private
15
+ def <<(obj)
16
+ @target.call(obj)
17
+ end
18
+ end
19
+ end
@@ -1,45 +1,54 @@
1
1
  module Elevate
2
- # Launches a new asynchronous task.
3
- #
4
- # @param args [Hash]
5
- # input arguments for the task, available to the +task+ block
6
- #
7
- # @return [NSOperation]
8
- # operation representing this task
9
- #
10
- # @api public
11
- def async(args = {}, &block)
12
- with_operation(args, block) do |operation|
13
- queue.addOperation(operation)
14
- end
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
15
4
  end
16
5
 
17
- private
6
+ module ClassMethods
7
+ def task(name, options = {}, &block)
8
+ task_definitions[name.to_sym] = TaskDefinition.new(name.to_sym, options, &block)
9
+ end
18
10
 
19
- def queue
20
- Dispatch.once do
21
- $elevate_queue = NSOperationQueue.alloc.init
22
- $elevate_queue.maxConcurrentOperationCount = 1
11
+ def task_definitions
12
+ @task_definitions ||= {}
23
13
  end
14
+ end
15
+
16
+ def cancel(name)
17
+ active_tasks.each do |task|
18
+ if task.name == name
19
+ task.cancel
20
+ end
21
+ end
22
+ end
24
23
 
25
- $elevate_queue
24
+ def cancel_all
25
+ active_tasks.each do |task|
26
+ task.cancel
27
+ end
26
28
  end
27
29
 
28
- def with_operation(args, dsl_block, &block)
29
- dsl = DSL.new(&dsl_block)
30
+ def launch(name, args = {})
31
+ raise ArgumentError, "args must be a Hash" unless args.is_a? Hash
32
+
33
+ definition = self.class.task_definitions[name.to_sym]
30
34
 
31
- raise "No task block specified!" unless dsl.task_callback
35
+ task = Task.new(definition, self, active_tasks)
36
+ task.start(args)
32
37
 
33
- operation = ElevateOperation.alloc.initWithTarget(dsl.task_callback, args: args)
34
- operation.on_start = Callback.new(self, dsl.start_callback) if dsl.start_callback
35
- operation.on_finish = Callback.new(self, dsl.finish_callback) if dsl.finish_callback
36
- operation.on_update = Callback.new(self, dsl.update_callback) if dsl.update_callback
37
- operation.on_timeout= Callback.new(self, dsl.timeout_callback) if dsl.timeout_callback
38
+ task
39
+ end
38
40
 
39
- operation.timeout = dsl.timeout_interval if dsl.timeout_interval
41
+ def task_args
42
+ @__elevate_task_args
43
+ end
40
44
 
41
- yield operation
45
+ def task_args=(args)
46
+ @__elevate_task_args = args
47
+ end
48
+
49
+ private
42
50
 
43
- operation
51
+ def active_tasks
52
+ @__elevate_active_tasks ||= []
44
53
  end
45
54
  end
@@ -1,28 +1,28 @@
1
1
  module Elevate
2
- class Promise
2
+ class Future
3
3
  OUTSTANDING = 0
4
4
  FULFILLED = 1
5
5
 
6
6
  def initialize
7
7
  @lock = NSConditionLock.alloc.initWithCondition(OUTSTANDING)
8
- @result = nil
8
+ @value = nil
9
9
  end
10
10
 
11
- def fulfill(result)
11
+ def fulfill(value)
12
12
  if @lock.tryLockWhenCondition(OUTSTANDING)
13
- @result = result
13
+ @value = value
14
14
  @lock.unlockWithCondition(FULFILLED)
15
15
  end
16
16
  end
17
17
 
18
18
  def value
19
- result = nil
19
+ value = nil
20
20
 
21
21
  @lock.lockWhenCondition(FULFILLED)
22
- result = @result
22
+ value = @value
23
23
  @lock.unlockWithCondition(FULFILLED)
24
24
 
25
- result
25
+ value
26
26
  end
27
27
  end
28
28
  end
@@ -76,11 +76,12 @@ module HTTP
76
76
  @request.setValue(value.to_s, forHTTPHeaderField:key.to_s)
77
77
  end
78
78
 
79
+ #@cache = self.class.cache
79
80
  @response = Response.new
80
81
  @response.url = url
81
82
 
82
83
  @connection = nil
83
- @promise = Promise.new
84
+ @future = Future.new
84
85
  end
85
86
 
86
87
  # Cancels an in-flight request.
@@ -93,10 +94,10 @@ module HTTP
93
94
  def cancel
94
95
  return unless sent?
95
96
 
96
- NetworkThread.cancel(@connection)
97
+ NetworkThread.cancel(@connection) if @connection
97
98
  ActivityIndicator.instance.hide
98
99
 
99
- @promise.fulfill(nil)
100
+ @future.fulfill(nil)
100
101
  end
101
102
 
102
103
  # Returns a response to this request, sending it if necessary
@@ -112,7 +113,7 @@ module HTTP
112
113
  send
113
114
  end
114
115
 
115
- @promise.value
116
+ @future.value
116
117
  end
117
118
 
118
119
  # Sends this request. The caller is not blocked.
@@ -122,6 +123,7 @@ module HTTP
122
123
  # @api public
123
124
  def send
124
125
  @connection = NSURLConnection.alloc.initWithRequest(@request, delegate:self, startImmediately:false)
126
+ @request = nil
125
127
 
126
128
  NetworkThread.start(@connection)
127
129
  ActivityIndicator.instance.show
@@ -139,6 +141,15 @@ module HTTP
139
141
 
140
142
  private
141
143
 
144
+ def self.cache
145
+ Dispatch.once do
146
+ @cache = NSURLCache.alloc.initWithMemoryCapacity(0, diskCapacity: 0, diskPath: nil)
147
+ NSURLCache.setSharedURLCache(cache)
148
+ end
149
+
150
+ @cache
151
+ end
152
+
142
153
  def connection(connection, didReceiveResponse: response)
143
154
  @response.headers = response.allHeaderFields
144
155
  @response.status_code = response.statusCode
@@ -149,22 +160,29 @@ module HTTP
149
160
  end
150
161
 
151
162
  def connection(connection, didFailWithError: error)
163
+ @connection = nil
164
+
152
165
  puts "ERROR: #{error.localizedDescription} (code: #{error.code})" unless RUBYMOTION_ENV == "test"
153
166
 
154
167
  @response.error = error
155
- @response.freeze
156
168
 
157
169
  ActivityIndicator.instance.hide
158
170
 
159
- @promise.fulfill(@response)
171
+ response = @response
172
+ @response = nil
173
+
174
+ @future.fulfill(response)
160
175
  end
161
176
 
162
177
  def connectionDidFinishLoading(connection)
163
- @response.freeze
178
+ @connection = nil
164
179
 
165
180
  ActivityIndicator.instance.hide
166
181
 
167
- @promise.fulfill(@response)
182
+ response = @response
183
+ @response = nil
184
+
185
+ @future.fulfill(response)
168
186
  end
169
187
 
170
188
  def connection(connection, willSendRequest: request, redirectResponse: response)