elevate 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ .repl_history
2
+ build
3
+ resources/*.nib
4
+ resources/*.momd
5
+ resources/*.storyboardc
data/.rvmrc ADDED
@@ -0,0 +1,5 @@
1
+ rvm ruby-1.9.3-p194@rubymotion --create
2
+
3
+ [[ -s "config/rvm_env.sh" ]] && . "config/rvm_env.sh"
4
+ [[ -s ".rvm_env" ]] && . ".rvm_env"
5
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ elevate (0.3)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ coderay (1.0.8)
10
+ guard (1.6.2)
11
+ listen (>= 0.6.0)
12
+ lumberjack (>= 1.0.2)
13
+ pry (>= 0.9.10)
14
+ terminal-table (>= 1.4.3)
15
+ thor (>= 0.14.6)
16
+ guard-motion (0.1.1)
17
+ guard (>= 1.1.0)
18
+ rake (>= 0.9)
19
+ listen (0.7.2)
20
+ lumberjack (1.0.2)
21
+ method_source (0.8.1)
22
+ pry (0.9.11.4)
23
+ coderay (~> 1.0.5)
24
+ method_source (~> 0.8)
25
+ slop (~> 3.4)
26
+ rake (0.9.6)
27
+ rb-fsevent (0.9.3)
28
+ slop (3.4.3)
29
+ terminal-table (1.4.5)
30
+ thor (0.17.0)
31
+ webstub (0.3.4)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ elevate!
38
+ guard-motion (~> 0.1.1)
39
+ rake (>= 0.9.0)
40
+ rb-fsevent (~> 0.9.1)
41
+ webstub (~> 0.3.3)
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'motion', :all_after_pass => false do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+
7
+ # RubyMotion gem example
8
+ watch(%r{^lib/[^/]+/(.+)\.rb$}) { |m| "./spec/#{m[1]}_spec.rb" }
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2013 Matt Green
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,179 @@
1
+ Elevate
2
+ ======
3
+ How do we convey the intent of our application?
4
+
5
+ Background
6
+ -----------
7
+ iOS applications employ the MVC architecture to delineate responsibilities:
8
+
9
+ * Models represent the entities important to our app
10
+ * Views display those entities
11
+ * Controllers react to user input, adjusting the model
12
+
13
+ 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.
14
+
15
+ 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.
16
+
17
+ Extracting use cases into their own class has several benefits:
18
+
19
+ * Consolidates domain and boundary interactions (read: all non-UI code) to a single conceptual unit
20
+ * Clarifies the intent of the code, both within the view controller and the use case
21
+ * Simplifies IO within the use case, allowing it feel blocking, while remaining interruptible (see below)
22
+ * Eases testing, allow you to employ either mock-based tests or acceptance tests
23
+
24
+ Implementation
25
+ --------------
26
+ 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.
27
+
28
+ The `Elevate::HTTP` module wraps NSURLRequest to work with this control flow. (Unfortunately, most iOS HTTP libraries do not work well with this paradigm.)
29
+
30
+ Example
31
+ ------------
32
+ Synchronizing data between a remote API and a local DB:
33
+
34
+ ```ruby
35
+ class SyncArtists < Action
36
+ def execute
37
+ stale_artists.each do |stale_artist|
38
+ artist = api.get_artist(stale_artist.name)
39
+ tracked_artists.add(artist)
40
+ end
41
+
42
+ tracked_artists.all
43
+ end
44
+
45
+ private
46
+ def stale_artists
47
+ stale = []
48
+
49
+ current = api.get_artists()
50
+ current.each do |artist|
51
+ if stale?(artist)
52
+ stale << artist
53
+ end
54
+ end
55
+
56
+ stale
57
+ end
58
+
59
+ def stale?(artist)
60
+ existing = tracked_artists.find_by_name(artist.name)
61
+ if existing.nil?
62
+ return true
63
+ end
64
+
65
+ existing.updated_at < artist.updated_at
66
+ end
67
+ end
68
+ ```
69
+
70
+ Notice the use case (`SyncArtists`) describes the algorithm at a high level. It is not concerned with the UI, and it depends on abstractions.
71
+
72
+ The view controller retains a similar focus. In fact, it is completely ignorant of how the sync algorithm operates. It only knows that it will return a list of artists to display:
73
+
74
+ ```ruby
75
+ class ArtistsViewController < UITableViewController
76
+ include Elevate
77
+
78
+ def artists
79
+ @artists ||= []
80
+ end
81
+
82
+ def artists=(artists)
83
+ @artists = artists
84
+ view.reloadData()
85
+ end
86
+
87
+ def viewWillAppear(animated)
88
+ super
89
+
90
+ async SyncArtists.new do
91
+ on_completed do |operation|
92
+ self.artists = operation.result
93
+ end
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ Requirements
100
+ ------------
101
+ * **iOS 6.x and higher** (due to `setDelegateQueue` being [horribly broken](http://openradar.appspot.com/10529053) on iOS 5.x.)
102
+
103
+ Installation
104
+ ------------
105
+ Update your Gemfile:
106
+
107
+ gem "elevate", "~> 0.3.0"
108
+
109
+ Bundle:
110
+
111
+ $ bundle install
112
+
113
+ Usage
114
+ -----
115
+
116
+ Write a use case. Use case classes must respond to `execute`. Anything returned from `execute` is made available to the controller callbacks:
117
+ ```ruby
118
+ class TrackArtist < Action
119
+ def initialize(artist_name)
120
+ @artist_name = artist_name
121
+ end
122
+
123
+ def execute
124
+ unless registration.completed?
125
+ user = api.register()
126
+ registration.save(user)
127
+ end
128
+
129
+ artist = api.track(@artist_name)
130
+ tracked_artists.add(artist)
131
+
132
+ artist
133
+ end
134
+ end
135
+ ```
136
+
137
+ Include the module in your view controller:
138
+
139
+ ```ruby
140
+ class ArtistsSearchViewController < UIViewController
141
+ include Elevate
142
+ ```
143
+
144
+ Execute a use case:
145
+
146
+ ```ruby
147
+ async TrackArtist.new(artist_name) do
148
+ on_started do |operation|
149
+ SVProgressHUD.showWithStatus("Adding...", maskType:SVProgressHUDMaskTypeGradient)
150
+ end
151
+
152
+ # operation.result contains the return value of #execute
153
+ # operation.exception contains the raised exception (if any)
154
+ on_completed do |operation|
155
+ SVProgressHUD.dismiss()
156
+ end
157
+ end
158
+ ```
159
+
160
+ Caveats
161
+ ---------
162
+ * **DSL is not finalized**
163
+ * Sending CoreData entities across threads is dangerous
164
+ * The callback DSL is clunky to try to avoid retain issues
165
+ * Must use Elevate's HTTP client instead of other iOS networking libs
166
+ * No way to report progress (idea: `execute` could yield status information via optional block)
167
+
168
+ Inspiration
169
+ -----------
170
+ * [PoEAA: Transaction Script](http://martinfowler.com/eaaCatalog/transactionScript.html)
171
+ * [The Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
172
+ * [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture)
173
+ * [Architecture: The Lost Years](http://www.youtube.com/watch?v=WpkDN78P884)
174
+ * [Android SDK's AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)
175
+ * Go (asynchronous IO done correctly)
176
+
177
+ License
178
+ ---------
179
+ MIT License
@@ -0,0 +1,20 @@
1
+ $:.unshift("/Library/RubyMotion/lib")
2
+
3
+ require 'motion/project'
4
+ require "bundler/gem_tasks"
5
+ require "bundler/setup"
6
+ Bundler.require :default
7
+
8
+ require 'webstub'
9
+
10
+ $:.unshift("./lib/")
11
+ require './lib/elevate'
12
+
13
+ Motion::Project::App.setup do |app|
14
+ app.name = "elevate"
15
+
16
+ if ENV["DEFAULT_PROVISIONING_PROFILE"]
17
+ app.provisioning_profile = ENV["DEFAULT_PROVISIONING_PROFILE"]
18
+ end
19
+ end
20
+
@@ -0,0 +1,5 @@
1
+ class AppDelegate
2
+ def application(application, didFinishLaunchingWithOptions:launchOptions)
3
+ true
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/elevate/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Matt Green"]
6
+ gem.email = ["mattgreenrocks@gmail.com"]
7
+ gem.description = "Distill the essence of your RubyMotion app"
8
+ gem.summary = "Distill the essence of your RubyMotion app"
9
+ gem.homepage = "http://github.com/mattgreen/elevate"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "elevate"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = Elevate::VERSION
16
+
17
+ gem.add_development_dependency 'rake', '>= 0.9.0'
18
+ gem.add_development_dependency 'guard-motion', '~> 0.1.1'
19
+ gem.add_development_dependency 'rb-fsevent', '~> 0.9.1'
20
+ gem.add_development_dependency 'webstub', '~> 0.3.3'
21
+ end
@@ -0,0 +1,11 @@
1
+ require 'elevate/version'
2
+
3
+ unless defined?(Motion::Project::Config)
4
+ raise "This file must be required within a RubyMotion project Rakefile."
5
+ end
6
+
7
+ Motion::Project::App.setup do |app|
8
+ Dir.glob(File.join(File.dirname(__FILE__), "elevate/**/*.rb")).each do |file|
9
+ app.files.unshift(file)
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ module Elevate
2
+ def async(target, &block)
3
+ with_operation(target, block) do |operation|
4
+ queue.addOperation(operation)
5
+ end
6
+ end
7
+
8
+ private
9
+
10
+ def queue
11
+ Dispatch.once do
12
+ $elevate_queue = NSOperationQueue.alloc.init
13
+ $elevate_queue.maxConcurrentOperationCount = 1
14
+ end
15
+
16
+ $elevate_queue
17
+ end
18
+
19
+ def with_operation(target, dsl_block, &block)
20
+ operation = ElevateOperation.alloc.initWithTarget(target)
21
+
22
+ if dsl_block
23
+ dsl = DSL.new(&dsl_block)
24
+
25
+ operation.on_started = Callback.new(self, operation, dsl.started_callback) if dsl.started_callback
26
+ operation.on_finished = Callback.new(self, operation, dsl.finished_callback) if dsl.finished_callback
27
+ end
28
+
29
+ yield operation
30
+
31
+ operation
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ module Elevate
2
+ class Callback
3
+ def initialize(context, operation, block)
4
+ @context = context
5
+ @operation = operation
6
+ @block = block
7
+ end
8
+
9
+ def call
10
+ @context.instance_exec(@operation, &@block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,18 @@
1
+ module Elevate
2
+ class DSL
3
+ def initialize(&block)
4
+ instance_eval(&block)
5
+ end
6
+
7
+ attr_reader :started_callback
8
+ attr_reader :finished_callback
9
+
10
+ def on_started(&block)
11
+ @started_callback = block
12
+ end
13
+
14
+ def on_completed(&block)
15
+ @finished_callback = block
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ module Elevate
2
+ module HTTP
3
+ module Base64
4
+ def self.encode(s)
5
+ [s].pack("m0")
6
+ end
7
+ end
8
+ end
9
+ end