bubble-wrap 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/LICENSE +26 -0
- data/README.md +147 -0
- data/Rakefile +1 -0
- data/bubble-wrap.gemspec +16 -0
- data/lib/bubble-wrap.rb +7 -0
- data/lib/bubble-wrap/app.rb +50 -0
- data/lib/bubble-wrap/gestures.rb +39 -0
- data/lib/bubble-wrap/http.rb +240 -0
- data/lib/bubble-wrap/json.rb +36 -0
- data/lib/bubble-wrap/kernel.rb +95 -0
- data/lib/bubble-wrap/ns_index_path.rb +23 -0
- data/lib/bubble-wrap/ns_notification_center.rb +22 -0
- data/lib/bubble-wrap/ns_user_defaults.rb +15 -0
- data/lib/bubble-wrap/ui_button.rb +7 -0
- data/lib/bubble-wrap/ui_view_controller.rb +11 -0
- data/lib/bubble-wrap/version.rb +3 -0
- data/spec/http_spec.rb +0 -0
- data/spec/json_spec.rb +116 -0
- data/spec/ns_notification_center_spec.rb +32 -0
- data/spec/spec_helper.rb +7 -0
- data/spec_helper_patch.diff +31 -0
- metadata +76 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
LICENCE
|
2
|
+
|
3
|
+
MIT: http://mattaimonetti.mit-license.org
|
4
|
+
|
5
|
+
------------------------------------------------
|
6
|
+
|
7
|
+
The MIT License (MIT)
|
8
|
+
Copyright © 2012 Matt Aimonetti <matt.aimonetti@gmail.com>
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the “Software”), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in
|
18
|
+
all copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
26
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# BubbleWrap for RubyMotion
|
2
|
+
|
3
|
+
A collection of helpers and wrappers used to wrap CocoaTouch code and provide more Ruby like APIs.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem install bubble-wrap
|
9
|
+
```
|
10
|
+
|
11
|
+
### Setup
|
12
|
+
|
13
|
+
1. Edit the Rakefile of your RubyMotion project and add the following require line.
|
14
|
+
```ruby
|
15
|
+
require 'bubble-wrap'
|
16
|
+
```
|
17
|
+
|
18
|
+
2. Now, you can use BubbleWrap extension in your app:
|
19
|
+
````ruby
|
20
|
+
class AppDelegate
|
21
|
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
22
|
+
puts "#{App.name} (#{documents_path})"
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
````
|
27
|
+
|
28
|
+
For a more complete list of helper/wrapper descriptions and more details, see the [wiki](https://github.com/mattetti/BubbleWrap/wiki).
|
29
|
+
|
30
|
+
## HTTP
|
31
|
+
|
32
|
+
`BubbleWrap::HTTP` wraps `NSURLRequest`, `NSURLConnection` and friends to provide Ruby developers with a more familiar and easier to use API.
|
33
|
+
The API uses async calls and blocks to stay as simple as possible.
|
34
|
+
|
35
|
+
Usage example:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
BubbleWrap::HTTP.get("https://api.github.com/users/mattetti") do |response|
|
39
|
+
p response.body.to_str
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
BubbleWrap::HTTP.get("https://api.github.com/users/mattetti", {credentials: {username: 'matt', password: 'aimonetti'}}) do |response|
|
45
|
+
p response.body.to_str # prints the response's body
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
data = {first_name: 'Matt', last_name: 'Aimonetti'}
|
51
|
+
BubbleWrap::HTTP.post("http://foo.bar.com/", {payload: data}) do |response|
|
52
|
+
if response.ok?
|
53
|
+
json = BubbleWrap::JSON.parse(response.body.to_str)
|
54
|
+
p json['id']
|
55
|
+
elsif response.status_code.to_s =~ /40\d/
|
56
|
+
alert("Login failed") # helper provided by the kernel file in this repo.
|
57
|
+
else
|
58
|
+
alert(response.error_message)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## JSON
|
64
|
+
|
65
|
+
`BubbleWrap::JSON` wraps `NSJSONSerialization` available in iOS5 and offers the same API as Ruby's JSON std lib.
|
66
|
+
|
67
|
+
## Kernel
|
68
|
+
|
69
|
+
A collection of useful methods used often in my RubyMotion apps.
|
70
|
+
|
71
|
+
Examples:
|
72
|
+
```ruby
|
73
|
+
> iphone?
|
74
|
+
# true
|
75
|
+
> ipad?
|
76
|
+
# false
|
77
|
+
> orientation
|
78
|
+
# :portrait
|
79
|
+
> simulator?
|
80
|
+
# true
|
81
|
+
> documents_path
|
82
|
+
# "/Users/mattetti/Library/Application Support/iPhone Simulator/5.0/Applications/EEC6454E-1816-451E-BB9A-EE18222E1A8F/Documents"
|
83
|
+
```
|
84
|
+
|
85
|
+
## App
|
86
|
+
|
87
|
+
A module allowing developers to store global states and also provides a
|
88
|
+
persistence layer.
|
89
|
+
|
90
|
+
## NSUserDefaults
|
91
|
+
|
92
|
+
Helper methods added to the class repsonsible for user preferences.
|
93
|
+
|
94
|
+
## NSIndexPath
|
95
|
+
|
96
|
+
Helper methods added to give `NSIndexPath` a bit more or a Ruby
|
97
|
+
interface.
|
98
|
+
|
99
|
+
## Gestures
|
100
|
+
|
101
|
+
Extra methods on `UIView` for working with gesture recognizers. A gesture recognizer can be added using a normal Ruby block, like so:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
view.whenTapped do
|
105
|
+
UIView.animateWithDuration(1,
|
106
|
+
animations:lambda {
|
107
|
+
# animate
|
108
|
+
# @view.transform = ...
|
109
|
+
})
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
There are similar methods for pinched, rotated, swiped, panned, and pressed (for long presses). All of the methods return the actual recognizer object, so it is possible to set the delegate if more fine-grained control is needed.
|
114
|
+
|
115
|
+
## UIButton
|
116
|
+
|
117
|
+
Helper methods to give `UIButton` a Ruby-like interface. Ex:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
button.when(UIControlEventTouchUpInside) do
|
121
|
+
self.view.backgroundColor = UIColor.redColor
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
## NSNotificationCenter
|
126
|
+
|
127
|
+
Helper methods to give NSNotificationCenter a Ruby-like interface:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
def viewWillAppear(animated)
|
131
|
+
notification_center.observe self, UIApplicationWillEnterForegroundNotification do
|
132
|
+
loadAndRefresh
|
133
|
+
end
|
134
|
+
|
135
|
+
notification_center.observe self, ReloadNotification do
|
136
|
+
loadAndRefresh
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def viewWillDisappear(animated)
|
141
|
+
notification_center.unobserve self
|
142
|
+
end
|
143
|
+
|
144
|
+
def reload
|
145
|
+
notification_center.post ReloadNotification
|
146
|
+
end
|
147
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bubble-wrap.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/bubble-wrap/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Matt Aimonetti", "Francis Chong"]
|
6
|
+
gem.email = ["mattaimonetti@gmail.com", "francis@ignition.hk"]
|
7
|
+
gem.description = "RubyMotion wrappers and helpers (Ruby for iOS) - Making Cocoa APIs more Ruby like, one API at a time. Fork away and send your pull request."
|
8
|
+
gem.summary = "RubyMotion wrappers and helpers (Ruby for iOS) - Making Cocoa APIs more Ruby like, one API at a time. Fork away and send your pull request."
|
9
|
+
gem.homepage = "https://github.com/mattetti/BubbleWrap"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "bubble-wrap"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = BubbleWrap::VERSION
|
16
|
+
end
|
data/lib/bubble-wrap.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Provides a module to store global states and a persistence layer.
|
2
|
+
#
|
3
|
+
module App
|
4
|
+
module_function
|
5
|
+
|
6
|
+
@states = {}
|
7
|
+
|
8
|
+
def states
|
9
|
+
@states
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
NSBundle.mainBundle.bundleIdentifier
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return application frame
|
17
|
+
def frame
|
18
|
+
UIScreen.mainScreen.applicationFrame
|
19
|
+
end
|
20
|
+
|
21
|
+
# Application Delegate
|
22
|
+
def delegate
|
23
|
+
UIApplication.sharedApplication.delegate
|
24
|
+
end
|
25
|
+
|
26
|
+
# Persistence module built on top of NSUserDefaults
|
27
|
+
module Persistence
|
28
|
+
def self.app_key
|
29
|
+
@app_key ||= App.name
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.[]=(key, value)
|
33
|
+
defaults = NSUserDefaults.standardUserDefaults
|
34
|
+
defaults.setObject(value, forKey: storage_key(key))
|
35
|
+
defaults.synchronize
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.[](key)
|
39
|
+
defaults = NSUserDefaults.standardUserDefaults
|
40
|
+
defaults.objectForKey storage_key(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def self.storage_key(key)
|
46
|
+
app_key + '_' + key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Opens UIView to add methods for working with gesture recognizers.
|
2
|
+
|
3
|
+
class UIView
|
4
|
+
|
5
|
+
def whenTapped(&proc)
|
6
|
+
addGestureRecognizerHelper(proc, UITapGestureRecognizer.alloc.initWithTarget(proc, action:'call'))
|
7
|
+
end
|
8
|
+
|
9
|
+
def whenPinched(&proc)
|
10
|
+
addGestureRecognizerHelper(proc, UIPinchGestureRecognizer.alloc.initWithTarget(proc, action:'call'))
|
11
|
+
end
|
12
|
+
|
13
|
+
def whenRotated(&proc)
|
14
|
+
addGestureRecognizerHelper(proc, UIRotationGestureRecognizer.alloc.initWithTarget(proc, action:'call'))
|
15
|
+
end
|
16
|
+
|
17
|
+
def whenSwiped(&proc)
|
18
|
+
addGestureRecognizerHelper(proc, UISwipeGestureRecognizer.alloc.initWithTarget(proc, action:'call'))
|
19
|
+
end
|
20
|
+
|
21
|
+
def whenPanned(&proc)
|
22
|
+
addGestureRecognizerHelper(proc, UIPanGestureRecognizer.alloc.initWithTarget(proc, action:'call'))
|
23
|
+
end
|
24
|
+
|
25
|
+
def whenPressed(&proc)
|
26
|
+
addGestureRecognizerHelper(proc, UILongPressGestureRecognizer.alloc.initWithTarget(proc, action:'call'))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Adds the recognizer and keeps a strong reference to the Proc object.
|
32
|
+
def addGestureRecognizerHelper(proc, recognizer)
|
33
|
+
self.addGestureRecognizer(recognizer)
|
34
|
+
@recognizers = {} unless @recognizers
|
35
|
+
@recognizers["#{proc}"] = proc
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
module BubbleWrap
|
2
|
+
|
3
|
+
SETTINGS = {}
|
4
|
+
|
5
|
+
# The HTTP module provides a simple interface to make HTTP requests.
|
6
|
+
#
|
7
|
+
# TODO: preflight support, easier/better cookie support, better error handling
|
8
|
+
module HTTP
|
9
|
+
|
10
|
+
# Make a GET request and process the response asynchronously via a block.
|
11
|
+
#
|
12
|
+
# @examples
|
13
|
+
# # Simple GET request printing the body
|
14
|
+
# BubbleWrap::HTTP.get("https://api.github.com/users/mattetti") do |response|
|
15
|
+
# p response.body.to_str
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # GET request with basic auth credentials
|
19
|
+
# BubbleWrap::HTTP.get("https://api.github.com/users/mattetti", {credentials: {username: 'matt', password: 'aimonetti'}}) do |response|
|
20
|
+
# p response.body.to_str # prints the response's body
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
def self.get(url, options={}, &block)
|
24
|
+
delegator = block_given? ? block : options.delete(:action)
|
25
|
+
HTTP::Query.new( url, :get, options.merge({:action => delegator}) )
|
26
|
+
end
|
27
|
+
|
28
|
+
# Make a POST request
|
29
|
+
def self.post(url, options={}, &block)
|
30
|
+
delegator = block_given? ? block : options.delete(:action)
|
31
|
+
HTTP::Query.new( url, :post, options.merge({:action => delegator}) )
|
32
|
+
end
|
33
|
+
|
34
|
+
# Make a PUT request
|
35
|
+
def self.put(url, options={}, &block)
|
36
|
+
delegator = block_given? ? block : options.delete(:action)
|
37
|
+
HTTP::Query.new( url, :put, options.merge({:action => delegator}) )
|
38
|
+
end
|
39
|
+
|
40
|
+
# Make a DELETE request
|
41
|
+
def self.delete(url, options={}, &block)
|
42
|
+
delegator = block_given? ? block : options.delete(:action)
|
43
|
+
HTTP::Query.new( url, :delete, options.merge({:action => delegator}) )
|
44
|
+
end
|
45
|
+
|
46
|
+
# Make a HEAD request
|
47
|
+
def self.head(url, options={}, &block)
|
48
|
+
delegator = block_given? ? block : options.delete(:action)
|
49
|
+
HTTP::Query.new( url, :head, options.merge({:action => delegator}) )
|
50
|
+
end
|
51
|
+
|
52
|
+
# Make a PATCH request
|
53
|
+
def self.patch(url, options={}, &block)
|
54
|
+
delegator = block_given? ? block : options.delete(:action)
|
55
|
+
HTTP::Query.new( url, :patch, options.merge({:action => delegator}) )
|
56
|
+
end
|
57
|
+
|
58
|
+
# Response class wrapping the results of a Query's response
|
59
|
+
class Response
|
60
|
+
attr_reader :body
|
61
|
+
attr_reader :headers
|
62
|
+
attr_accessor :status_code, :error_message
|
63
|
+
attr_reader :url
|
64
|
+
|
65
|
+
def initialize(values={})
|
66
|
+
self.update(values)
|
67
|
+
end
|
68
|
+
|
69
|
+
def update(values)
|
70
|
+
values.each do |k,v|
|
71
|
+
self.instance_variable_set("@#{k}", v)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def ok?
|
76
|
+
status_code == 200
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# Class wrapping NSConnection and often used indirectly by the BubbleWrap::HTTP module methods.
|
82
|
+
class Query
|
83
|
+
attr_accessor :request
|
84
|
+
attr_accessor :connection
|
85
|
+
attr_accessor :credentials # username & password has a hash
|
86
|
+
attr_accessor :proxy_credential # credential supplied to proxy servers
|
87
|
+
attr_accessor :post_data
|
88
|
+
attr_reader :method
|
89
|
+
|
90
|
+
attr_reader :response
|
91
|
+
attr_reader :status_code
|
92
|
+
attr_reader :response_headers
|
93
|
+
attr_reader :response_size
|
94
|
+
attr_reader :options
|
95
|
+
|
96
|
+
# ==== Parameters
|
97
|
+
# url<String>:: url of the resource to download
|
98
|
+
# http_method<Symbol>:: Value representing the HTTP method to use
|
99
|
+
# options<Hash>:: optional options used for the query
|
100
|
+
#
|
101
|
+
# ==== Options
|
102
|
+
# :payload<String> - data to pass to a POST, PUT, DELETE query.
|
103
|
+
# :delegator - Proc, class or object to call when the file is downloaded.
|
104
|
+
# a proc will receive a Response object while the passed object
|
105
|
+
# will receive the handle_query_response method
|
106
|
+
# :headers<Hash> - headers send with the request
|
107
|
+
# Anything else will be available via the options attribute reader.
|
108
|
+
#
|
109
|
+
def initialize(url, http_method = :get, options={})
|
110
|
+
@method = http_method.upcase.to_s
|
111
|
+
@delegator = options.delete(:action) || self
|
112
|
+
@payload = options.delete(:payload)
|
113
|
+
@credentials = options.delete(:credentials) || {}
|
114
|
+
@credentials = {:username => '', :password => ''}.merge(@credentials)
|
115
|
+
@timeout = options.delete(:timeout) || 30.0
|
116
|
+
headers = options.delete(:headers)
|
117
|
+
if headers
|
118
|
+
@headers = {}
|
119
|
+
headers.each{|k,v| @headers[k] = v.gsub("\n", '\\n') } # escaping LFs
|
120
|
+
end
|
121
|
+
@options = options
|
122
|
+
@response = HTTP::Response.new
|
123
|
+
initiate_request(url)
|
124
|
+
connection.start
|
125
|
+
connection
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate_get_params(payload, prefix=nil)
|
129
|
+
list = []
|
130
|
+
payload.each do |k,v|
|
131
|
+
if v.is_a?(Hash)
|
132
|
+
new_prefix = prefix ? "#{prefix}[#{k.to_s}]" : k.to_s
|
133
|
+
param = generate_get_params(v, new_prefix)
|
134
|
+
else
|
135
|
+
param = prefix ? "#{prefix}[#{k}]=#{v}" : "#{k}=#{v}"
|
136
|
+
end
|
137
|
+
list << param
|
138
|
+
end
|
139
|
+
return list.flatten
|
140
|
+
end
|
141
|
+
|
142
|
+
def initiate_request(url_string)
|
143
|
+
# http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/nsrunloop_Class/Reference/Reference.html#//apple_ref/doc/constant_group/Run_Loop_Modes
|
144
|
+
# NSConnectionReplyMode
|
145
|
+
|
146
|
+
unless @payload.nil?
|
147
|
+
if @payload.is_a?(Hash)
|
148
|
+
params = generate_get_params(@payload)
|
149
|
+
@payload = params.join("&")
|
150
|
+
end
|
151
|
+
url_string = "#{url_string}?#{@payload}" if @method == "GET"
|
152
|
+
end
|
153
|
+
|
154
|
+
p "HTTP building a NSRequest for #{url_string}"# if SETTINGS[:debug]
|
155
|
+
@url = NSURL.URLWithString(url_string)
|
156
|
+
@request = NSMutableURLRequest.requestWithURL(@url,
|
157
|
+
cachePolicy:NSURLRequestUseProtocolCachePolicy,
|
158
|
+
timeoutInterval:@timeout)
|
159
|
+
@request.setHTTPMethod @method
|
160
|
+
@request.setAllHTTPHeaderFields(@headers) if @headers
|
161
|
+
|
162
|
+
# @payload needs to be converted to data
|
163
|
+
unless @method == "GET" || @payload.nil?
|
164
|
+
@payload = @payload.to_s.dataUsingEncoding(NSUTF8StringEncoding)
|
165
|
+
@request.setHTTPBody @payload
|
166
|
+
end
|
167
|
+
|
168
|
+
# NSHTTPCookieStorage.sharedHTTPCookieStorage
|
169
|
+
|
170
|
+
@connection = NSURLConnection.connectionWithRequest(request, delegate:self)
|
171
|
+
@request.instance_variable_set("@done_loading", false)
|
172
|
+
def @request.done_loading; @done_loading; end
|
173
|
+
def @request.done_loading!; @done_loading = true; end
|
174
|
+
end
|
175
|
+
|
176
|
+
def connection(connection, didReceiveResponse:response)
|
177
|
+
@status_code = response.statusCode
|
178
|
+
@response_headers = response.allHeaderFields
|
179
|
+
@response_size = response.expectedContentLength.to_f
|
180
|
+
# p "HTTP status code: #{@status_code}, content length: #{@response_size}, headers: #{@response_headers}" if SETTINGS[:debug]
|
181
|
+
end
|
182
|
+
|
183
|
+
# This delegate method get called every time a chunk of data is being received
|
184
|
+
def connection(connection, didReceiveData:received_data)
|
185
|
+
@received_data ||= NSMutableData.new
|
186
|
+
@received_data.appendData(received_data)
|
187
|
+
end
|
188
|
+
|
189
|
+
def connection(connection, willSendRequest:request, redirectResponse:redirect_response)
|
190
|
+
puts "HTTP redirected #{request.description}" #if SETTINGS[:debug]
|
191
|
+
new_request = request.mutableCopy
|
192
|
+
# new_request.setValue(@credentials.inspect, forHTTPHeaderField:'Authorization') # disabled while we figure this one out
|
193
|
+
new_request.setAllHTTPHeaderFields(@headers) if @headers
|
194
|
+
@connection.cancel
|
195
|
+
@connection = NSURLConnection.connectionWithRequest(new_request, delegate:self)
|
196
|
+
new_request
|
197
|
+
end
|
198
|
+
|
199
|
+
def connection(connection, didFailWithError: error)
|
200
|
+
@request.done_loading!
|
201
|
+
p "HTTP Connection failed #{error.localizedDescription}"
|
202
|
+
@response.error_message = error.localizedDescription
|
203
|
+
if @delegator.respond_to?(:call)
|
204
|
+
@delegator.call( @response, self )
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# The transfer is done and everything went well
|
209
|
+
def connectionDidFinishLoading(connection)
|
210
|
+
@request.done_loading!
|
211
|
+
|
212
|
+
# copy the data in a local var that we will attach to the response object
|
213
|
+
response_body = NSData.dataWithData(@received_data) if @received_data
|
214
|
+
@response.update(status_code: status_code, body: response_body, headers: response_headers, url: @url)
|
215
|
+
# Don't reset the received data since this method can be called multiple times if the headers can report the wrong length.
|
216
|
+
# @received_data = nil
|
217
|
+
if @delegator.respond_to?(:call)
|
218
|
+
@delegator.call( @response, self )
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def connection(connection, didReceiveAuthenticationChallenge:challenge)
|
223
|
+
# p "HTTP auth required" if SETTINGS[:debug]
|
224
|
+
if (challenge.previousFailureCount == 0)
|
225
|
+
# by default we are keeping the credential for the entire session
|
226
|
+
# Eventually, it would be good to let the user pick one of the 3 possible credential persistence options:
|
227
|
+
# NSURLCredentialPersistenceNone,
|
228
|
+
# NSURLCredentialPersistenceForSession,
|
229
|
+
# NSURLCredentialPersistencePermanent
|
230
|
+
p "auth challenged, answered with credentials: #{credentials.inspect}"
|
231
|
+
new_credential = NSURLCredential.credentialWithUser(credentials[:username], password:credentials[:password], persistence:NSURLCredentialPersistenceForSession)
|
232
|
+
challenge.sender.useCredential(new_credential, forAuthenticationChallenge:challenge)
|
233
|
+
else
|
234
|
+
challenge.sender.cancelAuthenticationChallenge(challenge)
|
235
|
+
p 'Auth Failed :('
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BubbleWrap
|
2
|
+
|
3
|
+
# Handles JSON encoding and decoding in a similar way Ruby 1.9 does.
|
4
|
+
module JSON
|
5
|
+
|
6
|
+
class ParserError < StandardError; end
|
7
|
+
|
8
|
+
# Parses a string or data object and converts it in data structure.
|
9
|
+
#
|
10
|
+
# @param [String, NSData] str_data the string or data to serialize.
|
11
|
+
# @raise [ParserError] If the parsing of the passed string/data isn't valid.
|
12
|
+
# @return [Hash, Array, NilClass] the converted data structure, nil if the incoming string isn't valid.
|
13
|
+
#
|
14
|
+
# TODO: support options like the C Ruby module does
|
15
|
+
def self.parse(str_data, &block)
|
16
|
+
data = str_data.respond_to?(:to_data) ? str_data.to_data : str_data
|
17
|
+
opts = NSJSONReadingMutableContainers & NSJSONReadingMutableLeaves & NSJSONReadingAllowFragments
|
18
|
+
error = Pointer.new(:id)
|
19
|
+
obj = NSJSONSerialization.JSONObjectWithData(data, options:opts, error:error)
|
20
|
+
raise ParserError, error[0].description if error[0]
|
21
|
+
if block_given?
|
22
|
+
yield obj
|
23
|
+
else
|
24
|
+
obj
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.generate(obj)
|
30
|
+
# opts = NSJSONWritingPrettyPrinted
|
31
|
+
data = NSJSONSerialization.dataWithJSONObject(obj, options:0, error:nil)
|
32
|
+
data.to_str
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Kernel
|
2
|
+
|
3
|
+
# Verifies that the device running the app is an iPhone.
|
4
|
+
# @return [TrueClass, FalseClass] true will be returned if the device is an iPhone, false otherwise.
|
5
|
+
def iphone?
|
6
|
+
UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone
|
7
|
+
end
|
8
|
+
|
9
|
+
# Verifies that the device running the app is an iPad.
|
10
|
+
# @return [TrueClass, FalseClass] true will be returned if the device is an iPad, false otherwise.
|
11
|
+
def ipad?
|
12
|
+
UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad
|
13
|
+
end
|
14
|
+
|
15
|
+
# Verifies that the device running has a front facing camera.
|
16
|
+
# @return [TrueClass, FalseClass] true will be returned if the device has a front facing camera, false otherwise.
|
17
|
+
def front_camera?
|
18
|
+
UIImagePickerController.isCameraDeviceAvailable(UIImagePickerControllerCameraDeviceFront)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Verifies that the device running has a rear facing camera.
|
22
|
+
# @return [TrueClass, FalseClass] true will be returned if the device has a rear facing camera, false otherwise.
|
23
|
+
def rear_camera?
|
24
|
+
UIImagePickerController.isCameraDeviceAvailable(UIImagePickerControllerCameraDeviceRear)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the application's document directory path where users might be able to upload content.
|
28
|
+
# @return [String] the path to the document directory
|
29
|
+
def documents_path
|
30
|
+
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the application resource path where resource located
|
34
|
+
# @return [String] the application main bundle resource path
|
35
|
+
def resources_path
|
36
|
+
NSBundle.mainBundle.resourcePath
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the default notification center
|
40
|
+
# @return [NSNotificationCenter] the default notification center
|
41
|
+
def notification_center
|
42
|
+
NSNotificationCenter.defaultCenter
|
43
|
+
end
|
44
|
+
|
45
|
+
def orientation
|
46
|
+
case UIDevice.currentDevice.orientation
|
47
|
+
when UIDeviceOrientationUnknown then :unknown
|
48
|
+
when UIDeviceOrientationPortrait then :portrait
|
49
|
+
when UIDeviceOrientationPortraitUpsideDown then :portrait_upside_down
|
50
|
+
when UIDeviceOrientationLandscapeLeft then :landscape_left
|
51
|
+
when UIDeviceOrientationLandscapeRight then :landscape_right
|
52
|
+
when UIDeviceOrientationFaceUp then :face_up
|
53
|
+
when UIDeviceOrientationFaceDown then :face_down
|
54
|
+
else
|
55
|
+
:unknown
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [UIcolor]
|
60
|
+
def rgb_color(r,g,b)
|
61
|
+
rgba_color(r,g,b,1)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [UIcolor]
|
65
|
+
def rgba_color(r,g,b,a)
|
66
|
+
UIColor.colorWithRed((r/255.0), green:(g/255.0), blue:(b/255.0), alpha:a)
|
67
|
+
end
|
68
|
+
|
69
|
+
def NSLocalizedString(key, value)
|
70
|
+
NSBundle.mainBundle.localizedStringForKey(key, value:value, table:nil)
|
71
|
+
end
|
72
|
+
|
73
|
+
def user_cache
|
74
|
+
NSUserDefaults.standardUserDefaults
|
75
|
+
end
|
76
|
+
|
77
|
+
def alert(msg)
|
78
|
+
alert = UIAlertView.alloc.initWithTitle msg,
|
79
|
+
message: nil,
|
80
|
+
delegate: nil,
|
81
|
+
cancelButtonTitle: "OK",
|
82
|
+
otherButtonTitles: nil
|
83
|
+
alert.show
|
84
|
+
end
|
85
|
+
|
86
|
+
def simulator?
|
87
|
+
@simulator_state ||= !(UIDevice.currentDevice.model =~ /simulator/i).nil?
|
88
|
+
end
|
89
|
+
|
90
|
+
# I had issues with #p on the device, this is a temporary workaround
|
91
|
+
def p(arg)
|
92
|
+
NSLog arg.inspect
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class NSIndexPath
|
2
|
+
|
3
|
+
# Gives access to an index at a given position.
|
4
|
+
# @param [Integer] position to use to fetch the index
|
5
|
+
# @return [Integer] the index for the given position
|
6
|
+
def [](position)
|
7
|
+
raise ArgumentError unless position.is_a?(Integer)
|
8
|
+
indexAtPosition(position)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Provides an iterator taking a block following the common Ruby idiom.
|
12
|
+
# @param [Block]
|
13
|
+
# @return [NSIndexPath] the iterated object itself
|
14
|
+
def each
|
15
|
+
i = 0
|
16
|
+
until i == self.length
|
17
|
+
yield self.indexAtPosition(i)
|
18
|
+
i += 1
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class NSNotificationCenter
|
2
|
+
attr_reader :observers
|
3
|
+
|
4
|
+
def observe(observer, name, object=nil, &proc)
|
5
|
+
@observers ||= {}
|
6
|
+
@observers[observer] ||= []
|
7
|
+
@observers[observer] << proc
|
8
|
+
self.addObserver(proc, selector:'call', name:name, object:object)
|
9
|
+
end
|
10
|
+
|
11
|
+
def unobserve(observer)
|
12
|
+
return unless @observers[observer]
|
13
|
+
@observers[observer].each do |proc|
|
14
|
+
removeObserver(proc)
|
15
|
+
end
|
16
|
+
@observers.delete(observer)
|
17
|
+
end
|
18
|
+
|
19
|
+
def post(name, object=nil, info=nil)
|
20
|
+
self.postNotificationName(name, object: object, userInfo:info)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Reopens the NSUserDefaults class to add Array like accessors
|
2
|
+
# @see https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/nsuserdefaults_Class/Reference/Reference.html
|
3
|
+
class NSUserDefaults
|
4
|
+
|
5
|
+
# Retrieves the object for the passed key
|
6
|
+
def [](key)
|
7
|
+
self.objectForKey(key)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Sets the value for a given key and save it right away.
|
11
|
+
def []=(key, val)
|
12
|
+
self.setObject(val, forKey: key)
|
13
|
+
self.synchronize
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class UIViewController
|
2
|
+
# Short hand to get the content frame
|
3
|
+
#
|
4
|
+
# Return content frame: the application frame - navigation bar frame
|
5
|
+
def content_frame
|
6
|
+
app_frame = App.frame
|
7
|
+
navbar_height = self.navigationController.nil? ?
|
8
|
+
0 : self.navigationController.navigationBar.frame.size.height
|
9
|
+
CGRectMake(0, 0, app_frame.size.width, app_frame.size.height - navbar_height)
|
10
|
+
end
|
11
|
+
end
|
data/spec/http_spec.rb
ADDED
File without changes
|
data/spec/json_spec.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
describe "JSON" do
|
2
|
+
|
3
|
+
before do
|
4
|
+
@json_string = <<-EOS
|
5
|
+
{
|
6
|
+
"public_gists": 248,
|
7
|
+
"type": "User",
|
8
|
+
"blog": "http://merbist.com",
|
9
|
+
"location": "San Diego, CA",
|
10
|
+
"followers": 303,
|
11
|
+
"company": "LivingSocial",
|
12
|
+
"html_url": "https://github.com/mattetti",
|
13
|
+
"created_at": "2008-01-31T22:56:31Z",
|
14
|
+
"email": "mattaimonetti@gmail.com",
|
15
|
+
"hireable": true,
|
16
|
+
"gravatar_id": "c69521d6e22fc0bbd69337ec8b1698df",
|
17
|
+
"bio": "",
|
18
|
+
"public_repos": 137,
|
19
|
+
"following": 6,
|
20
|
+
"name": "Matt Aimonetti",
|
21
|
+
"login": "mattetti",
|
22
|
+
"url": "https://api.github.com/users/mattetti",
|
23
|
+
"id": 113,
|
24
|
+
"avatar_url": "https://secure.gravatar.com/avatar/c69521d6e22fc0bbd69337ec8b1698df?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png"
|
25
|
+
}
|
26
|
+
EOS
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "parsing a basic JSON string without block" do
|
30
|
+
|
31
|
+
before do
|
32
|
+
@parsed = BubbleWrap::JSON.parse(@json_string)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should convert a top object into a Ruby hash" do
|
36
|
+
obj = @parsed
|
37
|
+
obj.class.should == Hash
|
38
|
+
obj.keys.size.should == 19
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should properly convert integers values" do
|
42
|
+
@parsed["id"].is_a?(Integer).should == true
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should properly convert string values" do
|
46
|
+
@parsed["login"].is_a?(String).should == true
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should convert an array into a Ruby array" do
|
50
|
+
p Bacon::Counter.inspect
|
51
|
+
obj = BubbleWrap::JSON.parse("[1,2,3]")
|
52
|
+
obj.class.should == Array
|
53
|
+
obj.size.should == 3
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "parsing a basic JSON string with block" do
|
59
|
+
|
60
|
+
before do
|
61
|
+
BubbleWrap::JSON.parse(@json_string) do |parsed|
|
62
|
+
@parsed = parsed
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should convert a top object into a Ruby hash" do
|
67
|
+
obj = @parsed
|
68
|
+
obj.class.should == Hash
|
69
|
+
obj.keys.size.should == 19
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should properly convert integers values" do
|
73
|
+
@parsed["id"].is_a?(Integer).should == true
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should properly convert string values" do
|
77
|
+
@parsed["login"].is_a?(String).should == true
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should convert an array into a Ruby array" do
|
81
|
+
p Bacon::Counter.inspect
|
82
|
+
obj = BubbleWrap::JSON.parse("[1,2,3]")
|
83
|
+
obj.class.should == Array
|
84
|
+
obj.size.should == 3
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "generating a JSON string from an object" do
|
90
|
+
|
91
|
+
before do
|
92
|
+
@obj = { foo: 'bar',
|
93
|
+
'bar' => 'baz',
|
94
|
+
baz: 123,
|
95
|
+
foobar: [1,2,3],
|
96
|
+
foobaz: {a: 1, b: 2}
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should generate from a hash" do
|
101
|
+
json = BubbleWrap::JSON.generate(@obj)
|
102
|
+
json.class == String
|
103
|
+
json.should == "{\"foo\":\"bar\",\"bar\":\"baz\",\"baz\":123,\"foobar\":[1,2,3],\"foobaz\":{\"a\":1,\"b\":2}}"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should encode and decode and object losslessly" do
|
107
|
+
json = BubbleWrap::JSON.generate(@obj)
|
108
|
+
obj = BubbleWrap::JSON.parse(json)
|
109
|
+
obj.keys.sort.should == @obj.keys.sort
|
110
|
+
obj.values.sort.should == @obj.values.sort
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
describe "NSNotificationCenter" do
|
2
|
+
SampleNotification = "SampleNotification"
|
3
|
+
after do
|
4
|
+
@observer = Object.new
|
5
|
+
end
|
6
|
+
|
7
|
+
after do
|
8
|
+
notification_center.unobserve(@observer)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "return notification center" do
|
12
|
+
notification_center.should.not.be.nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it "add observer" do
|
16
|
+
notified = false
|
17
|
+
notification_center.observe(@observer, SampleNotification) do
|
18
|
+
notified = true
|
19
|
+
end
|
20
|
+
|
21
|
+
lambda {
|
22
|
+
notification_center.post SampleNotification
|
23
|
+
}.should.change { notified }
|
24
|
+
end
|
25
|
+
|
26
|
+
it "remove observer" do
|
27
|
+
lambda {
|
28
|
+
notification_center.observe(@observer, SampleNotification) {}
|
29
|
+
notification_center.unobserve(@observer)
|
30
|
+
}.should.not.change { notification_center.observers.keys.size }
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
diff --git a/lib/motion/project/builder.rb b/lib/motion/project/builder.rb
|
2
|
+
index 39956a8..3e5a646 100644
|
3
|
+
--- a/lib/motion/project/builder.rb
|
4
|
+
+++ b/lib/motion/project/builder.rb
|
5
|
+
@@ -143,6 +143,7 @@ module Motion; module Project;
|
6
|
+
if config.spec_mode
|
7
|
+
# Build spec files too, but sequentially.
|
8
|
+
objs << build_file.call(File.expand_path(File.join(File.dirname(__FILE__), '../spec.rb')))
|
9
|
+
+ objs << build_file.call(config.spec_helper) if File.exist?(config.spec_helper)
|
10
|
+
spec_objs = config.spec_files.map { |path| build_file.call(path) }
|
11
|
+
objs += spec_objs
|
12
|
+
end
|
13
|
+
diff --git a/lib/motion/project/config.rb b/lib/motion/project/config.rb
|
14
|
+
index 8c7ba43..d43d9a3 100644
|
15
|
+
--- a/lib/motion/project/config.rb
|
16
|
+
+++ b/lib/motion/project/config.rb
|
17
|
+
@@ -253,7 +253,13 @@ module Motion; module Project
|
18
|
+
end
|
19
|
+
|
20
|
+
def spec_files
|
21
|
+
- Dir.glob(File.join(specs_dir, '**', '*.rb'))
|
22
|
+
+ files = Dir.glob(File.join(specs_dir, '**', '*.rb'))
|
23
|
+
+ files.delete(spec_helper)
|
24
|
+
+ files
|
25
|
+
+ end
|
26
|
+
+
|
27
|
+
+ def spec_helper
|
28
|
+
+ File.join(specs_dir, 'spec_helper.rb')
|
29
|
+
end
|
30
|
+
|
31
|
+
def motiondir
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bubble-wrap
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Aimonetti
|
9
|
+
- Francis Chong
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-05-22 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description: RubyMotion wrappers and helpers (Ruby for iOS) - Making Cocoa APIs more
|
16
|
+
Ruby like, one API at a time. Fork away and send your pull request.
|
17
|
+
email:
|
18
|
+
- mattaimonetti@gmail.com
|
19
|
+
- francis@ignition.hk
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- .gitignore
|
25
|
+
- Gemfile
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- bubble-wrap.gemspec
|
30
|
+
- lib/bubble-wrap.rb
|
31
|
+
- lib/bubble-wrap/app.rb
|
32
|
+
- lib/bubble-wrap/gestures.rb
|
33
|
+
- lib/bubble-wrap/http.rb
|
34
|
+
- lib/bubble-wrap/json.rb
|
35
|
+
- lib/bubble-wrap/kernel.rb
|
36
|
+
- lib/bubble-wrap/ns_index_path.rb
|
37
|
+
- lib/bubble-wrap/ns_notification_center.rb
|
38
|
+
- lib/bubble-wrap/ns_user_defaults.rb
|
39
|
+
- lib/bubble-wrap/ui_button.rb
|
40
|
+
- lib/bubble-wrap/ui_view_controller.rb
|
41
|
+
- lib/bubble-wrap/version.rb
|
42
|
+
- spec/http_spec.rb
|
43
|
+
- spec/json_spec.rb
|
44
|
+
- spec/ns_notification_center_spec.rb
|
45
|
+
- spec/spec_helper.rb
|
46
|
+
- spec_helper_patch.diff
|
47
|
+
homepage: https://github.com/mattetti/BubbleWrap
|
48
|
+
licenses: []
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.15
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: RubyMotion wrappers and helpers (Ruby for iOS) - Making Cocoa APIs more Ruby
|
71
|
+
like, one API at a time. Fork away and send your pull request.
|
72
|
+
test_files:
|
73
|
+
- spec/http_spec.rb
|
74
|
+
- spec/json_spec.rb
|
75
|
+
- spec/ns_notification_center_spec.rb
|
76
|
+
- spec/spec_helper.rb
|