elevate 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -2
- data/README.md +71 -115
- data/lib/elevate/api.rb +8 -9
- data/lib/elevate/callback.rb +14 -5
- data/lib/elevate/dsl.rb +11 -0
- data/lib/elevate/operation.rb +35 -19
- data/lib/elevate/version.rb +1 -1
- data/spec/api_spec.rb +24 -4
- data/spec/callback_spec.rb +14 -4
- data/spec/dsl_spec.rb +2 -2
- data/spec/http/activity_indicator_spec.rb +44 -0
- data/spec/operation_spec.rb +11 -40
- metadata +7 -10
- data/.rvmrc +0 -5
- data/lib/elevate/dispatcher.rb +0 -53
- data/spec/dispatcher_spec.rb +0 -40
- data/spec/helpers/target.rb +0 -23
data/.gitignore
CHANGED
data/.travis.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
language: objective-c
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,111 +1,77 @@
|
|
1
|
-
Elevate
|
1
|
+
Elevate [![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate)
|
2
2
|
======
|
3
|
-
How do we convey the intent of our application?
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
[![Code Climate](https://codeclimate.com/github/mattgreen/elevate.png)](https://codeclimate.com/github/mattgreen/elevate)
|
8
|
-
|
9
|
-
|
10
|
-
Background
|
11
|
-
-----------
|
12
|
-
iOS applications employ the MVC architecture to delineate responsibilities:
|
13
|
-
|
14
|
-
* Models represent the entities important to our app
|
15
|
-
* Views display those entities
|
16
|
-
* Controllers react to user input, adjusting the model
|
17
|
-
|
18
|
-
However, iOS view controllers seem to attract complexity: not only do they coordinate models, but they also coordinate boundary objects (persistence mechanisms, backend APIs) and view-related concerns. This conflation of responsibilities shrouds the domain logic (that is, what your application does), making it harder to reason about and test. Pure model-related logic can be tested easily on its own, the difficulty arises when models interact with boundaries. Asynchronous behavior only makes it worse.
|
19
|
-
|
20
|
-
Elevate posits that the essence of your system are the use cases. They constitute the unique value that your application delivers. Their correctness is too important to be mixed in with presentation-level concerns. This results in a bit more code (one class per use case), but it allows the view controller to be relentlessly focused on view-related concerns.
|
21
|
-
|
22
|
-
Extracting use cases into their own class has several benefits:
|
23
|
-
|
24
|
-
* Consolidates domain and boundary interactions (read: all non-UI code) to a single conceptual unit
|
25
|
-
* Clarifies the intent of the code, both within the view controller and the use case
|
26
|
-
* Simplifies IO within the use case, allowing it feel blocking, while remaining interruptible (see below)
|
27
|
-
* Eases testing, allow you to employ either mock-based tests or acceptance tests
|
28
|
-
|
29
|
-
Implementation
|
30
|
-
--------------
|
31
|
-
Use cases are executed one at a time in a single, global `NSOperationQueue`. Each use case invocation is contained within a `NSOperation` subclass called `ElevateOperation`. `ElevateOperation` sets up an execution environment enabling compliant IO libraries to use traditional blocking control flows, rather than the traditional asynchronous style employed by iOS. Calls may be interrupted by invoking `cancel` on the `ElevateOperation`, triggering a `CancelledError` to be raised within the use case.
|
32
|
-
|
33
|
-
The `Elevate::HTTP` module wraps NSURLRequest to work with this control flow. (Unfortunately, most iOS HTTP libraries do not work well with this paradigm.)
|
4
|
+
Stop scattering your domain logic across your view controller. Consolidate it to a single conceptual unit with Elevate.
|
34
5
|
|
35
6
|
Example
|
36
|
-
|
37
|
-
Synchronizing data between a remote API and a local DB:
|
7
|
+
-------
|
38
8
|
|
39
9
|
```ruby
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
10
|
+
@login_task = async username: username.text, password: password.text do
|
11
|
+
task do
|
12
|
+
# This block runs on a background thread.
|
13
|
+
#
|
14
|
+
# The @username and @password instance variables correspond to the args
|
15
|
+
# passed into async. API is a thin wrapper class over Elevate::HTTP,
|
16
|
+
# which blocks until the request returns, yet can be interrupted.
|
17
|
+
credentials = API.login(@username, @password)
|
18
|
+
if credentials
|
19
|
+
UserRegistration.store(credentials.username, credentials.token)
|
45
20
|
end
|
46
21
|
|
47
|
-
|
22
|
+
# Return value of block is passed back to on_completed
|
23
|
+
credentials != nil
|
48
24
|
end
|
49
25
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
current = api.get_artists()
|
55
|
-
current.each do |artist|
|
56
|
-
if stale?(artist)
|
57
|
-
stale << artist
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
stale
|
26
|
+
on_started do
|
27
|
+
# This block runs on the UI thread after the operation has been queued.
|
28
|
+
SVProgressHUD.showWithStatus("Logging In...")
|
62
29
|
end
|
63
30
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
return true
|
68
|
-
end
|
31
|
+
on_completed do |result, exception|
|
32
|
+
# This block runs on the UI thread after the task block has finished.
|
33
|
+
SVProgressHUD.dismiss
|
69
34
|
|
70
|
-
|
35
|
+
if exception == nil
|
36
|
+
if result
|
37
|
+
alert("Logged in successfully!")
|
38
|
+
else
|
39
|
+
alert("Invalid username/password!")
|
40
|
+
end
|
41
|
+
else
|
42
|
+
alert(exception)
|
43
|
+
end
|
71
44
|
end
|
72
45
|
end
|
73
46
|
```
|
74
47
|
|
75
|
-
|
48
|
+
Background
|
49
|
+
-----------
|
50
|
+
Many iOS apps have fairly simple domain logic that is obscured by several programming 'taxes':
|
76
51
|
|
77
|
-
|
52
|
+
* UI management
|
53
|
+
* asynchronous network requests
|
54
|
+
* I/O-heavy operations, such as storing large datasets to disk
|
78
55
|
|
79
|
-
|
80
|
-
class ArtistsViewController < UITableViewController
|
81
|
-
include Elevate
|
56
|
+
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.
|
82
57
|
|
83
|
-
|
84
|
-
@artists ||= []
|
85
|
-
end
|
58
|
+
Elevate is a mini task queue for your iOS 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 when it starts and finishes. 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.
|
86
59
|
|
87
|
-
|
88
|
-
@artists = artists
|
89
|
-
view.reloadData()
|
90
|
-
end
|
60
|
+
In a sense, Elevate is almost a control-flow library: it bends the rules of iOS development a bit to ensure that the unique value your application provides is as clear as possible. This is most apparent with how Elevate handles network I/O: it provides a blocking HTTP client built from NSURLRequest for use within your tasks. This lets you write your tasks in a simple, blocking manner, while letting Elevate handle concerns relating to cancellation, and errors.
|
91
61
|
|
92
|
-
|
93
|
-
|
62
|
+
Features
|
63
|
+
--------
|
94
64
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
```
|
65
|
+
* Small, beautiful DSL for describing your tasks
|
66
|
+
* Actor-style concurrency
|
67
|
+
* Simplifies asynchronous HTTP requests when used with Elevate::HTTP
|
68
|
+
* Built atop of NSOperationQueue
|
103
69
|
|
104
70
|
Installation
|
105
71
|
------------
|
106
72
|
Update your Gemfile:
|
107
73
|
|
108
|
-
gem "elevate", "~> 0.
|
74
|
+
gem "elevate", "~> 0.4.0"
|
109
75
|
|
110
76
|
Bundle:
|
111
77
|
|
@@ -114,27 +80,6 @@ Bundle:
|
|
114
80
|
Usage
|
115
81
|
-----
|
116
82
|
|
117
|
-
Write a use case. Use case classes must respond to `execute`. Anything returned from `execute` is made available to the controller callbacks:
|
118
|
-
```ruby
|
119
|
-
class TrackArtist < Action
|
120
|
-
def initialize(artist_name)
|
121
|
-
@artist_name = artist_name
|
122
|
-
end
|
123
|
-
|
124
|
-
def execute
|
125
|
-
unless registration.completed?
|
126
|
-
user = api.register()
|
127
|
-
registration.save(user)
|
128
|
-
end
|
129
|
-
|
130
|
-
artist = api.track(@artist_name)
|
131
|
-
tracked_artists.add(artist)
|
132
|
-
|
133
|
-
artist
|
134
|
-
end
|
135
|
-
end
|
136
|
-
```
|
137
|
-
|
138
83
|
Include the module in your view controller:
|
139
84
|
|
140
85
|
```ruby
|
@@ -142,39 +87,50 @@ class ArtistsSearchViewController < UIViewController
|
|
142
87
|
include Elevate
|
143
88
|
```
|
144
89
|
|
145
|
-
|
90
|
+
Launch an async task with the `async` method:
|
91
|
+
|
92
|
+
* Pass all the data the task needs to operate (such as credentials or search terms) in to the `async` method.
|
93
|
+
* Define a block that contains a `task` block. The `task` block should contain all of your non-UI code. It will be run on a background thread. Any data passed into the `async` method will be available as instance variables, keyed by the provided hash key.
|
94
|
+
* Optionally, define `on_started` and `on_completed` blocks to run as the task starts and finishes. These are run in the UI thread, and should contain all of your UI code.
|
146
95
|
|
147
96
|
```ruby
|
148
|
-
async
|
149
|
-
|
150
|
-
|
97
|
+
@track_task = async artist: searchBar.text do
|
98
|
+
task do
|
99
|
+
artist = API.track(@artist)
|
100
|
+
ArtistDB.update(artist)
|
101
|
+
end
|
102
|
+
|
103
|
+
on_started do
|
104
|
+
SVProgressHUD.showWithStatus("Adding...")
|
151
105
|
end
|
152
106
|
|
153
|
-
|
154
|
-
|
155
|
-
on_completed do |operation|
|
156
|
-
SVProgressHUD.dismiss()
|
107
|
+
on_completed do |result, exception|
|
108
|
+
SVProgressHUD.dismiss
|
157
109
|
end
|
158
110
|
end
|
159
111
|
```
|
160
112
|
|
113
|
+
To cancel a task (like when the view controller is being dismissed), call `cancel` on the task returned by the `async` method. This causes a `CancelledError` to be raised within the task itself, which is handled by the Elevate runtime. This also prevents any callbacks you have defined from running.
|
114
|
+
|
115
|
+
**NOTE: Within tasks, do not access the UI or containing view controller! It is extremely dangerous to do so. You must pass data into the `async` method to use it safely.**
|
116
|
+
|
117
|
+
To Do
|
118
|
+
-----
|
119
|
+
* Need ability to set timeout for tasks
|
120
|
+
* More thought on the semantics
|
121
|
+
|
161
122
|
Caveats
|
162
123
|
---------
|
163
|
-
* **DSL is not finalized**
|
164
|
-
* Sending CoreData entities across threads is dangerous
|
165
|
-
* The callback DSL is clunky to try to avoid retain issues
|
166
124
|
* Must use Elevate's HTTP client instead of other iOS networking libs
|
167
125
|
* No way to report progress (idea: `execute` could yield status information via optional block)
|
168
126
|
|
169
127
|
Inspiration
|
170
128
|
-----------
|
171
|
-
* [PoEAA: Transaction Script](http://martinfowler.com/eaaCatalog/transactionScript.html)
|
172
|
-
* [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
173
129
|
* [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture)
|
174
|
-
* [Architecture: The Lost Years](http://www.youtube.com/watch?v=WpkDN78P884)
|
175
130
|
* [Android SDK's AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)
|
176
131
|
* Go (asynchronous IO done correctly)
|
177
132
|
|
178
133
|
License
|
179
134
|
---------
|
180
135
|
MIT License
|
136
|
+
|
data/lib/elevate/api.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Elevate
|
2
|
-
def async(
|
3
|
-
with_operation(
|
2
|
+
def async(args = {}, &block)
|
3
|
+
with_operation(args, block) do |operation|
|
4
4
|
queue.addOperation(operation)
|
5
5
|
end
|
6
6
|
end
|
@@ -16,15 +16,14 @@ module Elevate
|
|
16
16
|
$elevate_queue
|
17
17
|
end
|
18
18
|
|
19
|
-
def with_operation(
|
20
|
-
|
19
|
+
def with_operation(args, dsl_block, &block)
|
20
|
+
dsl = DSL.new(&dsl_block)
|
21
21
|
|
22
|
-
|
23
|
-
dsl = DSL.new(&dsl_block)
|
22
|
+
raise "No task block specified!" unless dsl.task_callback
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
operation = ElevateOperation.alloc.initWithTarget(dsl.task_callback, args: args)
|
25
|
+
operation.on_started = Callback.new(self, dsl.started_callback) if dsl.started_callback
|
26
|
+
operation.on_finished = Callback.new(self, dsl.finished_callback) if dsl.finished_callback
|
28
27
|
|
29
28
|
yield operation
|
30
29
|
|
data/lib/elevate/callback.rb
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
module Elevate
|
2
2
|
class Callback
|
3
|
-
def initialize(
|
4
|
-
@
|
5
|
-
@operation = operation
|
3
|
+
def initialize(controller, block)
|
4
|
+
@controller = controller
|
6
5
|
@block = block
|
7
6
|
end
|
8
7
|
|
9
|
-
def call
|
10
|
-
|
8
|
+
def call(*args)
|
9
|
+
if NSThread.isMainThread
|
10
|
+
invoke(*args)
|
11
|
+
else
|
12
|
+
Dispatch::Queue.main.sync { invoke(*args) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def invoke(*args)
|
19
|
+
@controller.instance_exec(*args, &@block)
|
11
20
|
end
|
12
21
|
end
|
13
22
|
end
|
data/lib/elevate/dsl.rb
CHANGED
@@ -6,12 +6,23 @@ module Elevate
|
|
6
6
|
|
7
7
|
attr_reader :started_callback
|
8
8
|
attr_reader :finished_callback
|
9
|
+
attr_reader :task_callback
|
10
|
+
|
11
|
+
def task(&block)
|
12
|
+
raise "task blocks must accept zero parameters" unless block.arity == 0
|
13
|
+
|
14
|
+
@task_callback = block
|
15
|
+
end
|
9
16
|
|
10
17
|
def on_started(&block)
|
18
|
+
raise "on_started blocks must accept zero parameters" unless block.arity == 0
|
19
|
+
|
11
20
|
@started_callback = block
|
12
21
|
end
|
13
22
|
|
14
23
|
def on_completed(&block)
|
24
|
+
raise "on_completed blocks must accept two parameters" unless block.arity == 2
|
25
|
+
|
15
26
|
@finished_callback = block
|
16
27
|
end
|
17
28
|
end
|
data/lib/elevate/operation.rb
CHANGED
@@ -1,16 +1,32 @@
|
|
1
1
|
module Elevate
|
2
|
+
class Context
|
3
|
+
def initialize(args, &block)
|
4
|
+
metaclass = class << self; self; end
|
5
|
+
metaclass.send(:define_method, :execute, &block)
|
6
|
+
|
7
|
+
args.each do |key, value|
|
8
|
+
instance_variable_set("@#{key}", value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
2
13
|
class ElevateOperation < NSOperation
|
3
|
-
def initWithTarget(target)
|
14
|
+
def initWithTarget(target, args:args)
|
4
15
|
if init()
|
5
|
-
@target = target
|
6
16
|
@coordinator = IOCoordinator.new
|
7
|
-
@
|
17
|
+
@target = target
|
18
|
+
@context = Context.new(args, &target)
|
19
|
+
@finished_callback = nil
|
8
20
|
|
9
21
|
setCompletionBlock(lambda do
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
22
|
+
if @finished_callback
|
23
|
+
@finished_callback.call(@result, @exception) unless isCancelled
|
24
|
+
end
|
25
|
+
|
26
|
+
Dispatch::Queue.main.sync do
|
27
|
+
@target = nil
|
28
|
+
@finished_callback = nil
|
29
|
+
end
|
14
30
|
end)
|
15
31
|
end
|
16
32
|
|
@@ -18,13 +34,7 @@ module Elevate
|
|
18
34
|
end
|
19
35
|
|
20
36
|
def cancel
|
21
|
-
@coordinator.cancel
|
22
|
-
|
23
|
-
super
|
24
|
-
end
|
25
|
-
|
26
|
-
def dealloc
|
27
|
-
#puts 'dealloc!'
|
37
|
+
@coordinator.cancel
|
28
38
|
|
29
39
|
super
|
30
40
|
end
|
@@ -44,18 +54,18 @@ module Elevate
|
|
44
54
|
def main
|
45
55
|
log " START: #{inspect}"
|
46
56
|
|
47
|
-
@coordinator.install
|
57
|
+
@coordinator.install
|
48
58
|
|
49
59
|
begin
|
50
60
|
unless @coordinator.cancelled?
|
51
|
-
@result = @
|
61
|
+
@result = @context.execute
|
52
62
|
end
|
53
63
|
|
54
64
|
rescue Exception => e
|
55
65
|
@exception = e
|
56
66
|
end
|
57
67
|
|
58
|
-
@coordinator.uninstall
|
68
|
+
@coordinator.uninstall
|
59
69
|
|
60
70
|
log "FINISH: #{inspect}"
|
61
71
|
end
|
@@ -64,11 +74,17 @@ module Elevate
|
|
64
74
|
attr_reader :result
|
65
75
|
|
66
76
|
def on_started=(callback)
|
67
|
-
|
77
|
+
started_callback = callback
|
78
|
+
started_callback.retain
|
79
|
+
|
80
|
+
Dispatch::Queue.main.async do
|
81
|
+
started_callback.call unless isCancelled
|
82
|
+
started_callback.release
|
83
|
+
end
|
68
84
|
end
|
69
85
|
|
70
86
|
def on_finished=(callback)
|
71
|
-
@
|
87
|
+
@finished_callback = callback
|
72
88
|
end
|
73
89
|
end
|
74
90
|
end
|
data/lib/elevate/version.rb
CHANGED
data/spec/api_spec.rb
CHANGED
@@ -6,11 +6,14 @@ end
|
|
6
6
|
|
7
7
|
describe Elevate do
|
8
8
|
describe "#async" do
|
9
|
-
it "runs the specified
|
9
|
+
it "runs the specified task asynchronously" do
|
10
|
+
async do
|
11
|
+
task do
|
12
|
+
true
|
13
|
+
end
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
@called = true
|
15
|
+
on_completed do |result, exception|
|
16
|
+
@called = result
|
14
17
|
resume
|
15
18
|
end
|
16
19
|
end
|
@@ -19,5 +22,22 @@ describe Elevate do
|
|
19
22
|
@called.should.be.true
|
20
23
|
end
|
21
24
|
end
|
25
|
+
|
26
|
+
it "passes provided args to the task as instance variables" do
|
27
|
+
async name: "harry" do
|
28
|
+
task do
|
29
|
+
@name
|
30
|
+
end
|
31
|
+
|
32
|
+
on_completed do |name, exception|
|
33
|
+
@result = name
|
34
|
+
resume
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
wait_max 1.0 do
|
39
|
+
@result.should == "harry"
|
40
|
+
end
|
41
|
+
end
|
22
42
|
end
|
23
43
|
end
|
data/spec/callback_spec.rb
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
describe Elevate::Callback do
|
2
2
|
describe "#call" do
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
describe "on the main thread" do
|
4
|
+
it "invokes the block within the provided context" do
|
5
|
+
callback = Elevate::Callback.new(self, lambda { |v| @value = v })
|
6
|
+
callback.call(42)
|
6
7
|
|
7
|
-
|
8
|
+
@value.should == 42
|
9
|
+
end
|
8
10
|
end
|
9
11
|
|
12
|
+
describe "on a background thread" do
|
13
|
+
it "invokes the block within the provided context on the main thread" do
|
14
|
+
callback = Elevate::Callback.new(self, lambda { @thread = NSThread.currentThread })
|
15
|
+
callback.call
|
16
|
+
|
17
|
+
@thread.should == NSThread.mainThread
|
18
|
+
end
|
19
|
+
end
|
10
20
|
end
|
11
21
|
end
|
12
22
|
|
data/spec/dsl_spec.rb
CHANGED
@@ -2,7 +2,7 @@ describe Elevate::DSL do
|
|
2
2
|
describe "#on_started" do
|
3
3
|
it "stores the provided block" do
|
4
4
|
i = Elevate::DSL.new do
|
5
|
-
on_started {
|
5
|
+
on_started { puts 'hi' }
|
6
6
|
end
|
7
7
|
|
8
8
|
i.started_callback.should.not.be.nil
|
@@ -12,7 +12,7 @@ describe Elevate::DSL do
|
|
12
12
|
describe "#on_completed" do
|
13
13
|
it "stores the passed block" do
|
14
14
|
i = Elevate::DSL.new do
|
15
|
-
on_completed { |
|
15
|
+
on_completed { |result, exception| puts 'hi' }
|
16
16
|
end
|
17
17
|
|
18
18
|
i.finished_callback.should.not.be.nil
|
@@ -0,0 +1,44 @@
|
|
1
|
+
describe Elevate::HTTP::ActivityIndicator do
|
2
|
+
before do
|
3
|
+
UIApplication.sharedApplication.setNetworkActivityIndicatorVisible(false)
|
4
|
+
|
5
|
+
@indicator = Elevate::HTTP::ActivityIndicator.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe ".instance" do
|
9
|
+
it "returns a singleton instance" do
|
10
|
+
instance = Elevate::HTTP::ActivityIndicator.instance
|
11
|
+
instance2 = Elevate::HTTP::ActivityIndicator.instance
|
12
|
+
|
13
|
+
instance.object_id.should == instance2.object_id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#hide" do
|
18
|
+
it "does nothing if it isn't shown" do
|
19
|
+
@indicator.hide
|
20
|
+
|
21
|
+
UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.false
|
22
|
+
end
|
23
|
+
|
24
|
+
it "hides the indicator only if there are no outstanding show requests" do
|
25
|
+
@indicator.show
|
26
|
+
|
27
|
+
@indicator.show
|
28
|
+
@indicator.hide
|
29
|
+
|
30
|
+
UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.true
|
31
|
+
|
32
|
+
@indicator.hide
|
33
|
+
UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#show" do
|
38
|
+
it "shows the activity indicator" do
|
39
|
+
@indicator.show
|
40
|
+
|
41
|
+
UIApplication.sharedApplication.isNetworkActivityIndicatorVisible.should.be.true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/operation_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
describe Elevate::ElevateOperation do
|
2
2
|
before do
|
3
|
-
@target =
|
4
|
-
@operation = Elevate::ElevateOperation.alloc.initWithTarget(@target)
|
3
|
+
@target = lambda { @result }
|
4
|
+
@operation = Elevate::ElevateOperation.alloc.initWithTarget(@target, args: {})
|
5
5
|
@queue = NSOperationQueue.alloc.init
|
6
6
|
end
|
7
7
|
|
@@ -18,7 +18,7 @@ describe Elevate::ElevateOperation do
|
|
18
18
|
@lock = NSLock.alloc.init
|
19
19
|
@value = []
|
20
20
|
|
21
|
-
@operation.on_started
|
21
|
+
@operation.on_started = lambda do
|
22
22
|
@lock.lock()
|
23
23
|
if @value == []
|
24
24
|
@value << 1
|
@@ -26,14 +26,14 @@ describe Elevate::ElevateOperation do
|
|
26
26
|
@lock.unlock()
|
27
27
|
end
|
28
28
|
|
29
|
-
@operation.on_finished = lambda do
|
29
|
+
@operation.on_finished = lambda do |result, exception|
|
30
30
|
@lock.lock()
|
31
31
|
if @value == [1]
|
32
32
|
@value << 2
|
33
33
|
end
|
34
34
|
@lock.unlock()
|
35
35
|
|
36
|
-
resume
|
36
|
+
Dispatch::Queue.main.sync { resume }
|
37
37
|
end
|
38
38
|
|
39
39
|
@queue.addOperation(@operation)
|
@@ -58,50 +58,21 @@ describe Elevate::ElevateOperation do
|
|
58
58
|
|
59
59
|
describe "when an exception is raised" do
|
60
60
|
it "returns the exception" do
|
61
|
-
@target
|
61
|
+
@target = lambda { raise IndexError }
|
62
|
+
@operation = Elevate::ElevateOperation.alloc.initWithTarget(@target, args: {})
|
62
63
|
|
63
64
|
@queue.addOperation(@operation)
|
64
65
|
@operation.waitUntilFinished()
|
65
66
|
|
66
|
-
@operation.exception.should
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
describe "#main" do
|
72
|
-
describe "when the operation has not been run" do
|
73
|
-
it "invokes the target" do
|
74
|
-
@queue.addOperation(@operation)
|
75
|
-
@operation.waitUntilFinished()
|
76
|
-
|
77
|
-
@target.called.should == 1
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
describe "when the operation has been cancelled prior to starting" do
|
82
|
-
it "does not invoke the target" do
|
83
|
-
@operation.cancel()
|
84
|
-
|
85
|
-
@queue.addOperation(@operation)
|
86
|
-
@operation.waitUntilFinished()
|
87
|
-
|
88
|
-
@target.called.should == 0
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
describe "when the operation is running" do
|
93
|
-
it "allows IO to be cancelled" do
|
94
|
-
@queue.addOperation(@operation)
|
95
|
-
@operation.waitUntilFinished()
|
96
|
-
|
97
|
-
@target.io_coordinator.should.not.be.nil
|
67
|
+
@operation.exception.should.not.be.nil
|
98
68
|
end
|
99
69
|
end
|
100
70
|
end
|
101
71
|
|
102
72
|
describe "#result" do
|
103
73
|
before do
|
104
|
-
@target
|
74
|
+
@target = lambda { 42 }
|
75
|
+
@operation = Elevate::ElevateOperation.alloc.initWithTarget(@target, args: {})
|
105
76
|
end
|
106
77
|
|
107
78
|
describe "before starting the operation" do
|
@@ -122,7 +93,7 @@ describe Elevate::ElevateOperation do
|
|
122
93
|
end
|
123
94
|
|
124
95
|
describe "when the operation has finished" do
|
125
|
-
it "returns the result of the
|
96
|
+
it "returns the result of the lambda" do
|
126
97
|
@queue.addOperation(@operation)
|
127
98
|
@operation.waitUntilFinished()
|
128
99
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elevate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-04-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -83,7 +83,7 @@ extensions: []
|
|
83
83
|
extra_rdoc_files: []
|
84
84
|
files:
|
85
85
|
- .gitignore
|
86
|
-
- .
|
86
|
+
- .travis.yml
|
87
87
|
- Gemfile
|
88
88
|
- Gemfile.lock
|
89
89
|
- Guardfile
|
@@ -95,7 +95,6 @@ files:
|
|
95
95
|
- lib/elevate.rb
|
96
96
|
- lib/elevate/api.rb
|
97
97
|
- lib/elevate/callback.rb
|
98
|
-
- lib/elevate/dispatcher.rb
|
99
98
|
- lib/elevate/dsl.rb
|
100
99
|
- lib/elevate/http/activity_indicator.rb
|
101
100
|
- lib/elevate/http/base64.rb
|
@@ -111,9 +110,8 @@ files:
|
|
111
110
|
- lib/elevate/version.rb
|
112
111
|
- spec/api_spec.rb
|
113
112
|
- spec/callback_spec.rb
|
114
|
-
- spec/dispatcher_spec.rb
|
115
113
|
- spec/dsl_spec.rb
|
116
|
-
- spec/
|
114
|
+
- spec/http/activity_indicator_spec.rb
|
117
115
|
- spec/http/http_client_spec.rb
|
118
116
|
- spec/http/http_request_spec.rb
|
119
117
|
- spec/io_coordinator_spec.rb
|
@@ -132,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
132
130
|
version: '0'
|
133
131
|
segments:
|
134
132
|
- 0
|
135
|
-
hash:
|
133
|
+
hash: -1464207392655779665
|
136
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
135
|
none: false
|
138
136
|
requirements:
|
@@ -141,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
139
|
version: '0'
|
142
140
|
segments:
|
143
141
|
- 0
|
144
|
-
hash:
|
142
|
+
hash: -1464207392655779665
|
145
143
|
requirements: []
|
146
144
|
rubyforge_project:
|
147
145
|
rubygems_version: 1.8.24
|
@@ -151,9 +149,8 @@ summary: Distill the essence of your RubyMotion app
|
|
151
149
|
test_files:
|
152
150
|
- spec/api_spec.rb
|
153
151
|
- spec/callback_spec.rb
|
154
|
-
- spec/dispatcher_spec.rb
|
155
152
|
- spec/dsl_spec.rb
|
156
|
-
- spec/
|
153
|
+
- spec/http/activity_indicator_spec.rb
|
157
154
|
- spec/http/http_client_spec.rb
|
158
155
|
- spec/http/http_request_spec.rb
|
159
156
|
- spec/io_coordinator_spec.rb
|
data/.rvmrc
DELETED
data/lib/elevate/dispatcher.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
module Elevate
|
2
|
-
class Dispatcher
|
3
|
-
def initialize
|
4
|
-
@on_finished = nil
|
5
|
-
@on_started = nil
|
6
|
-
end
|
7
|
-
|
8
|
-
def dispose
|
9
|
-
return if @on_started.nil? && @on_finished.nil?
|
10
|
-
|
11
|
-
# Callbacks must be released on the main thread, because they may contain a strong
|
12
|
-
# reference to a UIKit component. See "The Deallocation Problem" for more info.
|
13
|
-
unless NSThread.isMainThread
|
14
|
-
self.performSelectorOnMainThread(:dispose, withObject: nil, waitUntilDone: true)
|
15
|
-
return
|
16
|
-
end
|
17
|
-
|
18
|
-
@on_started = nil
|
19
|
-
@on_finished = nil
|
20
|
-
end
|
21
|
-
|
22
|
-
def invoke_finished_callback
|
23
|
-
invoke(:@on_finished)
|
24
|
-
end
|
25
|
-
|
26
|
-
def on_finished=(callback)
|
27
|
-
@on_finished = callback
|
28
|
-
end
|
29
|
-
|
30
|
-
def on_started=(callback)
|
31
|
-
@on_started = callback
|
32
|
-
|
33
|
-
Dispatch::Queue.main.async do
|
34
|
-
invoke(:@on_started)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def invoke(callback_name)
|
41
|
-
unless NSThread.isMainThread
|
42
|
-
self.performSelectorOnMainThread(:"invoke:", withObject: callback_name, waitUntilDone: true)
|
43
|
-
return
|
44
|
-
end
|
45
|
-
|
46
|
-
if callback = instance_variable_get(callback_name)
|
47
|
-
callback.call()
|
48
|
-
|
49
|
-
instance_variable_set(callback_name, nil)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
data/spec/dispatcher_spec.rb
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
describe Elevate::Dispatcher do
|
2
|
-
DELAY = 0.2
|
3
|
-
|
4
|
-
before do
|
5
|
-
@dispatcher = Elevate::Dispatcher.new
|
6
|
-
end
|
7
|
-
|
8
|
-
describe "#on_started=" do
|
9
|
-
before do
|
10
|
-
@dispatcher.on_started = lambda { @thread = NSThread.currentThread }
|
11
|
-
end
|
12
|
-
|
13
|
-
after do
|
14
|
-
@thread = nil
|
15
|
-
end
|
16
|
-
|
17
|
-
it "invokes the callback on the main thread" do
|
18
|
-
wait DELAY do
|
19
|
-
@thread.should == NSThread.currentThread
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
it "does not invoke the callback immediately" do
|
24
|
-
@thread.should.be.nil
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
describe "#on_finished=" do
|
29
|
-
before do
|
30
|
-
@dispatcher.on_finished = lambda { @thread = NSThread.currentThread }
|
31
|
-
@dispatcher.invoke_finished_callback()
|
32
|
-
end
|
33
|
-
|
34
|
-
it "invokes the callback on the main thread" do
|
35
|
-
wait DELAY do
|
36
|
-
@thread.should == NSThread.currentThread
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
data/spec/helpers/target.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
class Target
|
2
|
-
attr_reader :called
|
3
|
-
attr_reader :io_coordinator
|
4
|
-
attr_accessor :exception
|
5
|
-
attr_accessor :result
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@called = 0
|
9
|
-
@result = true
|
10
|
-
end
|
11
|
-
|
12
|
-
def execute
|
13
|
-
@io_coordinator = Thread.current[:io_coordinator]
|
14
|
-
|
15
|
-
@called += 1
|
16
|
-
|
17
|
-
if @exception
|
18
|
-
raise @exception
|
19
|
-
end
|
20
|
-
|
21
|
-
@result
|
22
|
-
end
|
23
|
-
end
|