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 +4 -4
- data/README.md +70 -116
- data/elevate.gemspec +2 -1
- data/lib/elevate/channel.rb +19 -0
- data/lib/elevate/elevate.rb +39 -30
- data/lib/elevate/{promise.rb → future.rb} +7 -7
- data/lib/elevate/http/request.rb +26 -8
- data/lib/elevate/http/response.rb +1 -2
- data/lib/elevate/operation.rb +11 -144
- data/lib/elevate/task.rb +122 -0
- data/lib/elevate/task_context.rb +16 -7
- data/lib/elevate/task_definition.rb +62 -0
- data/lib/elevate/version.rb +1 -1
- data/spec/elevate_spec.rb +258 -106
- data/spec/operation_spec.rb +10 -47
- data/spec/task_context_spec.rb +12 -0
- metadata +21 -19
- data/lib/elevate/callback.rb +0 -22
- data/lib/elevate/dsl.rb +0 -49
- data/spec/callback_spec.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2fa20b0d90cf439fac88fe64dc15375b1ba2dba
|
4
|
+
data.tar.gz: 0c1df21032e6d395b64290813029b00fe8b00f9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
[](https://codeclimate.com/github/mattgreen/elevate) [](https://travis-ci.org/mattgreen/elevate) [](http://badge.fury.io/rb/elevate)
|
7
9
|
|
8
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
24
|
-
|
30
|
+
def viewDidLoad
|
31
|
+
super
|
25
32
|
|
26
|
-
|
27
|
-
credentials != nil
|
33
|
+
launch(:update)
|
28
34
|
end
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
36
|
+
task :update do
|
37
|
+
background do
|
38
|
+
response = Elevate::HTTP.get("https://example.org/artists")
|
39
|
+
DB.update(response)
|
34
40
|
|
35
|
-
|
36
|
-
|
37
|
-
puts status
|
38
|
-
end
|
41
|
+
response
|
42
|
+
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
-
*
|
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
|
-
|
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
|
-
|
65
|
+
(Some tasks may not need steps 1 or 3, of course.)
|
68
66
|
|
69
|
-
|
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.
|
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
|
-
*
|
158
|
-
*
|
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
|
---------
|
data/elevate.gemspec
CHANGED
@@ -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'
|
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
|
data/lib/elevate/elevate.rb
CHANGED
@@ -1,45 +1,54 @@
|
|
1
1
|
module Elevate
|
2
|
-
|
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
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
24
|
+
def cancel_all
|
25
|
+
active_tasks.each do |task|
|
26
|
+
task.cancel
|
27
|
+
end
|
26
28
|
end
|
27
29
|
|
28
|
-
def
|
29
|
-
|
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
|
-
|
35
|
+
task = Task.new(definition, self, active_tasks)
|
36
|
+
task.start(args)
|
32
37
|
|
33
|
-
|
34
|
-
|
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
|
-
|
41
|
+
def task_args
|
42
|
+
@__elevate_task_args
|
43
|
+
end
|
40
44
|
|
41
|
-
|
45
|
+
def task_args=(args)
|
46
|
+
@__elevate_task_args = args
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
42
50
|
|
43
|
-
|
51
|
+
def active_tasks
|
52
|
+
@__elevate_active_tasks ||= []
|
44
53
|
end
|
45
54
|
end
|
@@ -1,28 +1,28 @@
|
|
1
1
|
module Elevate
|
2
|
-
class
|
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
|
-
@
|
8
|
+
@value = nil
|
9
9
|
end
|
10
10
|
|
11
|
-
def fulfill(
|
11
|
+
def fulfill(value)
|
12
12
|
if @lock.tryLockWhenCondition(OUTSTANDING)
|
13
|
-
@
|
13
|
+
@value = value
|
14
14
|
@lock.unlockWithCondition(FULFILLED)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
def value
|
19
|
-
|
19
|
+
value = nil
|
20
20
|
|
21
21
|
@lock.lockWhenCondition(FULFILLED)
|
22
|
-
|
22
|
+
value = @value
|
23
23
|
@lock.unlockWithCondition(FULFILLED)
|
24
24
|
|
25
|
-
|
25
|
+
value
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
data/lib/elevate/http/request.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
171
|
+
response = @response
|
172
|
+
@response = nil
|
173
|
+
|
174
|
+
@future.fulfill(response)
|
160
175
|
end
|
161
176
|
|
162
177
|
def connectionDidFinishLoading(connection)
|
163
|
-
@
|
178
|
+
@connection = nil
|
164
179
|
|
165
180
|
ActivityIndicator.instance.hide
|
166
181
|
|
167
|
-
@
|
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)
|