elevate 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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)