bubble-wrap 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +6 -14
  2. data/.travis.yml +2 -1
  3. data/CHANGELOG.md +26 -2
  4. data/GETTING_STARTED.md +1 -1
  5. data/Gemfile.lock +1 -1
  6. data/HACKING.md +6 -4
  7. data/README.md +83 -3
  8. data/Rakefile +9 -7
  9. data/lib/bubble-wrap/all.rb +2 -2
  10. data/lib/bubble-wrap/mail.rb +9 -0
  11. data/lib/bubble-wrap/sms.rb +9 -0
  12. data/lib/bubble-wrap/ui.rb +6 -2
  13. data/lib/bubble-wrap/version.rb +1 -1
  14. data/motion/core.rb +1 -1
  15. data/motion/core/app.rb +7 -2
  16. data/motion/core/device/ios/camera.rb +3 -2
  17. data/motion/core/device/ios/screen.rb +19 -0
  18. data/motion/core/ios/app.rb +13 -1
  19. data/motion/core/ios/device.rb +5 -0
  20. data/motion/core/json.rb +1 -3
  21. data/motion/core/kvo.rb +11 -13
  22. data/motion/core/persistence.rb +8 -1
  23. data/motion/core/string.rb +3 -3
  24. data/motion/core/time.rb +5 -0
  25. data/motion/http.rb +1 -1
  26. data/motion/http/query.rb +39 -19
  27. data/motion/http/response.rb +4 -4
  28. data/motion/mail/mail.rb +59 -0
  29. data/motion/mail/result.rb +29 -0
  30. data/motion/reactor/eventable.rb +3 -2
  31. data/motion/reactor/periodic_timer.rb +12 -8
  32. data/motion/reactor/timer.rb +11 -7
  33. data/motion/sms/result.rb +24 -0
  34. data/motion/sms/sms.rb +47 -0
  35. data/motion/ui/pollute.rb +4 -1
  36. data/motion/ui/ui_alert_view.rb +15 -8
  37. data/motion/ui/ui_control_wrapper.rb +16 -0
  38. data/motion/ui/ui_view_controller_wrapper.rb +13 -0
  39. data/motion/ui/ui_view_wrapper.rb +55 -0
  40. data/resources/Localizable.strings +1 -0
  41. data/samples/gesture/Gemfile +2 -2
  42. data/samples/location/Gemfile +1 -1
  43. data/samples/osx/Gemfile +2 -2
  44. data/spec/motion/core/app_spec.rb +6 -0
  45. data/spec/motion/core/device/ios/camera_spec.rb +3 -3
  46. data/spec/motion/core/device/ios/screen_spec.rb +45 -0
  47. data/spec/motion/core/ios/app_spec.rb +29 -0
  48. data/spec/motion/core/persistence_spec.rb +25 -0
  49. data/spec/motion/core/string_spec.rb +2 -2
  50. data/spec/motion/core/time_spec.rb +14 -4
  51. data/spec/motion/core_spec.rb +9 -8
  52. data/spec/motion/http/query_spec.rb +92 -15
  53. data/spec/motion/http/response_spec.rb +4 -3
  54. data/spec/motion/location/location_spec.rb +1 -1
  55. data/spec/motion/mail/mail_spec.rb +125 -0
  56. data/spec/motion/mail/result_spec.rb +53 -0
  57. data/spec/motion/reactor/eventable_spec.rb +85 -0
  58. data/spec/motion/reactor_spec.rb +0 -20
  59. data/spec/motion/sms/result_spec.rb +38 -0
  60. data/spec/motion/sms/sms_spec.rb +71 -0
  61. data/spec/motion/ui/pollute_spec.rb +13 -0
  62. data/spec/motion/ui/ui_bar_button_item_spec.rb +8 -8
  63. data/spec/motion/ui/ui_control_wrapper_spec.rb +35 -0
  64. data/spec/motion/ui/ui_view_controller_wrapper_spec.rb +34 -0
  65. data/spec/motion/ui/ui_view_wrapper_spec.rb +62 -0
  66. data/travis.sh +7 -0
  67. metadata +52 -22
  68. data/motion/ui/gestures.rb +0 -57
  69. data/motion/ui/ui_control.rb +0 -14
  70. data/motion/ui/ui_view_controller.rb +0 -11
  71. data/spec/motion/ui/gestures_spec.rb +0 -62
  72. data/spec/motion/ui/ui_control_spec.rb +0 -42
@@ -63,9 +63,21 @@ module BubbleWrap
63
63
  UIApplication.sharedApplication
64
64
  end
65
65
 
66
+ def windows
67
+ UIApplication.sharedApplication.windows
68
+ end
69
+
66
70
  # the Application Window
67
71
  def window
68
- UIApplication.sharedApplication.keyWindow || UIApplication.sharedApplication.windows[0]
72
+ normal_windows = App.windows.select { |w|
73
+ w.windowLevel == UIWindowLevelNormal
74
+ }
75
+
76
+ key_window = normal_windows.select {|w|
77
+ w == UIApplication.sharedApplication.keyWindow
78
+ }.first
79
+
80
+ key_window || normal_windows.first
69
81
  end
70
82
  end
71
83
  end
@@ -55,5 +55,10 @@ module BubbleWrap
55
55
  def orientation
56
56
  screen.orientation
57
57
  end
58
+
59
+ # Delegates to BubbleWrap::Screen.interface_orientation
60
+ def interface_orientation
61
+ screen.interface_orientation
62
+ end
58
63
  end
59
64
  end
@@ -28,9 +28,7 @@ module BubbleWrap
28
28
  end
29
29
 
30
30
  def self.generate(obj)
31
- # opts = NSJSONWritingPrettyPrinted
32
- data = NSJSONSerialization.dataWithJSONObject(obj, options:0, error:nil)
33
- data.to_str
31
+ NSJSONSerialization.dataWithJSONObject(obj, options:0, error:nil).to_str
34
32
  end
35
33
 
36
34
  end
@@ -64,9 +64,7 @@ module BubbleWrap
64
64
  return if @targets.nil? || target.nil? || key_path.nil?
65
65
 
66
66
  key_paths = @targets[target]
67
- if !key_paths.nil? && key_paths.has_key?(key_path.to_s)
68
- key_paths.delete(key_path.to_s)
69
- end
67
+ key_paths.delete(key_path.to_s) if !key_paths.nil?
70
68
  end
71
69
 
72
70
  def remove_all_observer_blocks
@@ -75,19 +73,19 @@ module BubbleWrap
75
73
 
76
74
  # NSKeyValueObserving Protocol
77
75
 
78
- def observeValueForKeyPath(key_path, ofObject:target, change:change, context:context)
76
+ def observeValueForKeyPath(key_path, ofObject: target, change: change, context: context)
79
77
  key_paths = @targets[target] || {}
80
- blocks = key_paths[key_path] || []
81
- blocks.each do |block|
82
- args = [ change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey] ]
83
- args << change[NSKeyValueChangeIndexesKey] if collection?(change)
84
- block.call(*args)
85
- end
78
+ blocks = key_paths[key_path] || []
79
+ blocks.each do |block|
80
+ args = [change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]]
81
+ args << change[NSKeyValueChangeIndexesKey] if collection?(change)
82
+ block.call(*args)
86
83
  end
84
+ end
87
85
 
88
- def collection?(change)
89
- COLLECTION_OPERATIONS.include?(change[NSKeyValueChangeKindKey])
90
- end
86
+ def collection?(change)
87
+ COLLECTION_OPERATIONS.include?(change[NSKeyValueChangeKindKey])
88
+ end
91
89
 
92
90
  end
93
91
  end
@@ -29,12 +29,19 @@ module BubbleWrap
29
29
  storage.synchronize
30
30
  end
31
31
 
32
+ def delete(key)
33
+ value = storage.objectForKey storage_key(key)
34
+ storage.removeObjectForKey(storage_key(key))
35
+ storage.synchronize
36
+ value
37
+ end
38
+
32
39
  def storage
33
40
  NSUserDefaults.standardUserDefaults
34
41
  end
35
42
 
36
43
  def storage_key(key)
37
- app_key + '_' + key.to_s
44
+ "#{app_key}_#{key}"
38
45
  end
39
46
  end
40
47
 
@@ -55,9 +55,9 @@ module BubbleWrap
55
55
  hex_color = self.gsub("#", "")
56
56
  case hex_color.size
57
57
  when 3
58
- colors = hex_color.scan(%r{[0-9A-Fa-f]}).map{ |el| (el * 2).to_i(16) }
58
+ colors = hex_color.scan(%r{[0-9A-Fa-f]}).map!{ |el| (el * 2).to_i(16) }
59
59
  when 6
60
- colors = hex_color.scan(%r<[0-9A-Fa-f]{2}>).map{ |el| el.to_i(16) }
60
+ colors = hex_color.scan(%r<[0-9A-Fa-f]{2}>).map!{ |el| el.to_i(16) }
61
61
  else
62
62
  raise ArgumentError
63
63
  end
@@ -71,4 +71,4 @@ module BubbleWrap
71
71
  end
72
72
  end
73
73
 
74
- String.send(:include, BubbleWrap::String)
74
+ NSString.send(:include, BubbleWrap::String)
@@ -4,6 +4,11 @@ class Time
4
4
  cached_date_formatter("yyyy-MM-dd'T'HH:mm:ss'Z'").
5
5
  dateFromString(time)
6
6
  end
7
+
8
+ def self.iso8601_with_timezone(time)
9
+ cached_date_formatter("yyyy-MM-dd'T'HH:mm:ssZZZZZ").
10
+ dateFromString(time)
11
+ end
7
12
 
8
13
  private
9
14
 
@@ -19,7 +19,7 @@ module BubbleWrap
19
19
  # end
20
20
  #
21
21
 
22
- [:get, :post, :put, :delete, :head, :patch].each do |http_verb|
22
+ [:get, :post, :put, :delete, :head, :options, :patch].each do |http_verb|
23
23
 
24
24
  define_singleton_method(http_verb) do |url, options = {}, &block|
25
25
  options[:action] = block if block
@@ -1,5 +1,5 @@
1
1
  # Class wrapping NSConnection and often used indirectly by the BubbleWrap::HTTP module methods.
2
- class BubbleWrap::HTTP::Query
2
+ module BubbleWrap; module HTTP; class Query
3
3
  attr_accessor :request
4
4
  attr_accessor :connection
5
5
  attr_accessor :credentials # username & password has a hash
@@ -20,7 +20,7 @@ class BubbleWrap::HTTP::Query
20
20
  #
21
21
  # ==== Options
22
22
  # :payload<String> - data to pass to a POST, PUT, DELETE query.
23
- # :delegator - Proc, class or object to call when the file is downloaded.
23
+ # :action - Proc, class or object to call when the file is downloaded.
24
24
  # a proc will receive a Response object while the passed object
25
25
  # will receive the handle_query_response method
26
26
  # :headers<Hash> - headers send with the request
@@ -31,6 +31,7 @@ class BubbleWrap::HTTP::Query
31
31
  @method = http_method.upcase.to_s
32
32
  @delegator = options.delete(:action) || self
33
33
  @payload = options.delete(:payload)
34
+ @encoding = options.delete(:encoding) || NSUTF8StringEncoding
34
35
  @files = options.delete(:files)
35
36
  @boundary = options.delete(:boundary) || BW.create_uuid
36
37
  @credentials = options.delete(:credentials) || {}
@@ -70,9 +71,7 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
70
71
  if App.osx? && !response.is_a?(NSHTTPURLResponse)
71
72
  return
72
73
  end
73
- @status_code = response.statusCode
74
- @response_headers = response.allHeaderFields
75
- @response_size = response.expectedContentLength.to_f
74
+ did_receive_response(response)
76
75
  end
77
76
 
78
77
  # This delegate method get called every time a chunk of data is being received
@@ -95,7 +94,11 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
95
94
  log "##{@redirect_count} HTTP redirect_count: #{request.inspect} - #{self.description}"
96
95
 
97
96
  if @redirect_count >= 30
98
- @response.error_message = "Too many redirections"
97
+ @response.error = NSError.errorWithDomain('BubbleWrap::HTTP', code:NSURLErrorHTTPTooManyRedirects,
98
+ userInfo:NSDictionary.dictionaryWithObject("Too many redirections",
99
+ forKey: NSLocalizedDescriptionKey))
100
+ @response.error_message = @response.error.localizedDescription
101
+ show_status_indicator false
99
102
  @request.done_loading!
100
103
  call_delegator_with_response
101
104
  nil
@@ -109,6 +112,7 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
109
112
  log "HTTP Connection to #{@url.absoluteString} failed #{error.localizedDescription}"
110
113
  show_status_indicator false
111
114
  @request.done_loading!
115
+ @response.error = error
112
116
  @response.error_message = error.localizedDescription
113
117
  call_delegator_with_response
114
118
  end
@@ -139,15 +143,27 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
139
143
  log "auth challenged, answered with credentials: #{credentials.inspect}"
140
144
  end
141
145
  else
146
+ did_receive_response(challenge.failureResponse)
147
+ @response.update(status_code: status_code, headers: response_headers, url: @url, original_url: @original_url)
142
148
  challenge.sender.cancelAuthenticationChallenge(challenge)
143
149
  log 'Auth Failed :('
144
150
  end
145
151
  end
146
152
 
153
+ def cancel
154
+ @connection.cancel
155
+ show_status_indicator false
156
+ @request.done_loading!
157
+ end
147
158
 
148
159
  private
149
160
 
150
- private
161
+ def did_receive_response(response)
162
+ @status_code = response.statusCode
163
+ @response_headers = response.allHeaderFields
164
+ @response_size = response.expectedContentLength.to_f
165
+ end
166
+
151
167
  def show_status_indicator(show)
152
168
  if App.ios?
153
169
  UIApplication.sharedApplication.networkActivityIndicatorVisible = show
@@ -173,7 +189,7 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
173
189
 
174
190
  def set_content_type
175
191
  return if headers_provided?
176
- return if (@method == "GET" || @method == "HEAD")
192
+ return if (@method == "GET" || @method == "HEAD" || @method == "OPTIONS")
177
193
  @headers ||= {}
178
194
  @headers["Content-Type"] = case @format
179
195
  when :json
@@ -200,7 +216,7 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
200
216
  end
201
217
 
202
218
  def create_request_body
203
- return nil if (@method == "GET" || @method == "HEAD")
219
+ return nil if (@method == "GET" || @method == "HEAD" || @method == "OPTIONS")
204
220
  return nil unless (@payload || @files)
205
221
 
206
222
  body = NSMutableData.data
@@ -217,7 +233,10 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
217
233
  if @payload.is_a?(NSData)
218
234
  body.appendData(@payload)
219
235
  elsif @payload.is_a?(String)
220
- body.appendData(@payload.dataUsingEncoding NSUTF8StringEncoding)
236
+ body.appendData(@payload.to_encoded_data @encoding)
237
+ elsif @format == :json
238
+ json_string = BW::JSON.generate(@payload)
239
+ body.appendData(json_string.to_encoded_data @encoding)
221
240
  else
222
241
  append_form_params(body)
223
242
  end
@@ -231,7 +250,7 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
231
250
  s += "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
232
251
  s += value.to_s
233
252
  s += "\r\n"
234
- body.appendData(s.dataUsingEncoding NSUTF8StringEncoding)
253
+ body.appendData(s.to_encoded_data @encoding)
235
254
  end
236
255
  @payload_or_files_were_appended = true
237
256
  body
@@ -251,7 +270,7 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
251
270
 
252
271
  def parse_file(key, value)
253
272
  if value.is_a?(Hash)
254
- raise InvalidFileError if value[:data].nil?
273
+ raise(InvalidFileError, "You need to supply a `:data` entry in #{value} for file '#{key}' in your HTTP `:files`") if value[:data].nil?
255
274
  {data: value[:data], filename: value[:filename] || key}
256
275
  else
257
276
  {data: value, filename: key}
@@ -265,9 +284,9 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
265
284
  s += "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{file[:filename]}\"\r\n"
266
285
  s += "Content-Type: application/octet-stream\r\n\r\n"
267
286
  file_data = NSMutableData.new
268
- file_data.appendData(s.dataUsingEncoding NSUTF8StringEncoding)
287
+ file_data.appendData(s.to_encoded_data @encoding)
269
288
  file_data.appendData(file[:data])
270
- file_data.appendData("\r\n".dataUsingEncoding NSUTF8StringEncoding)
289
+ file_data.appendData("\r\n".to_encoded_data @encoding)
271
290
  body.appendData(file_data)
272
291
  end
273
292
  @payload_or_files_were_appended = true
@@ -275,12 +294,12 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
275
294
  end
276
295
 
277
296
  def append_body_boundary(body)
278
- body.appendData("--#{@boundary}--\r\n".dataUsingEncoding NSUTF8StringEncoding)
297
+ body.appendData("--#{@boundary}--\r\n".to_encoded_data @encoding)
279
298
  end
280
299
 
281
300
  def create_url(url_string)
282
301
  url_string = url_string.stringByAddingPercentEscapesUsingEncoding NSUTF8StringEncoding
283
- if (@method == "GET" || @method == "HEAD") && @payload
302
+ if (@method == "GET" || @method == "HEAD" || @method == "OPTIONS") && @payload
284
303
  unless @payload.empty?
285
304
  convert_payload_to_url if @payload.is_a?(Hash)
286
305
  url_string += "?#{@payload}"
@@ -299,8 +318,9 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
299
318
  end
300
319
 
301
320
  def escape(string)
302
- if string
303
- CFURLCreateStringByAddingPercentEscapes nil, string.to_s, nil, "!*'();:@&=+$,/?%#[]", KCFStringEncodingUTF8
321
+ string_to_escape = string.to_s
322
+ if string_to_escape
323
+ CFURLCreateStringByAddingPercentEscapes nil, string_to_escape, nil, "!*'();:@&=+$,/?%#[]", KCFStringEncodingUTF8
304
324
  end
305
325
  end
306
326
 
@@ -364,4 +384,4 @@ Cache policy: #{@cache_policy}, response: #{@response.inspect} >"
364
384
  NSURLConnection.connectionWithRequest(request, delegate:delegate)
365
385
  end
366
386
 
367
- end
387
+ end; end; end
@@ -1,8 +1,8 @@
1
1
  # Response class wrapping the results of a Query's response
2
- class BubbleWrap::HTTP::Response
2
+ module BubbleWrap; module HTTP; class Response
3
3
  attr_reader :body
4
4
  attr_reader :headers
5
- attr_accessor :status_code, :status_description, :error_message
5
+ attr_accessor :status_code, :status_description, :error_message, :error
6
6
  attr_reader :url
7
7
  attr_reader :original_url
8
8
 
@@ -18,7 +18,7 @@ class BubbleWrap::HTTP::Response
18
18
  end
19
19
 
20
20
  def ok?
21
- status_code.to_s =~ /20\d/ ? true : false
21
+ status_code.to_s =~ /2\d\d/ ? true : false
22
22
  end
23
23
 
24
24
  def to_s
@@ -29,4 +29,4 @@ class BubbleWrap::HTTP::Response
29
29
  def update_status_description
30
30
  @status_description = status_code.nil? ? nil : NSHTTPURLResponse.localizedStringForStatusCode(status_code)
31
31
  end
32
- end
32
+ end; end; end
@@ -0,0 +1,59 @@
1
+ module BubbleWrap
2
+ module Mail
3
+
4
+ module_function
5
+
6
+ # Base method to create your in-app mail
7
+ # ---------------------------------------
8
+ # EX
9
+ # BW::Mail.compose {
10
+ # delegate: self, # optional, will use root view controller by default
11
+ # to: [ "tom@example.com" ],
12
+ # cc: [ "itchy@example.com", "scratchy@example.com" ],
13
+ # bcc: [ "jerry@example.com" ],
14
+ # html: false,
15
+ # subject: "My Subject",
16
+ # message: "This is my message. It isn't very long.",
17
+ # animated: false
18
+ # } do |result, error|
19
+ # result.sent? # => boolean
20
+ # result.canceled? # => boolean
21
+ # result.saved? # => boolean
22
+ # result.failed? # => boolean
23
+ # error # => NSError
24
+ # end
25
+ def compose(options={}, &callback)
26
+ @delegate = options[:delegate] || App.window.rootViewController
27
+
28
+ @callback = callback
29
+
30
+ @mail_controller = create_mail_controller(options)
31
+
32
+ @mailer_is_animated = options[:animated] == false ? false : true
33
+ @delegate.presentModalViewController(@mail_controller, animated: @mailer_is_animated)
34
+ end
35
+
36
+ def create_mail_controller(options={})
37
+ mail_controller = MFMailComposeViewController.alloc.init
38
+
39
+ mail_controller.mailComposeDelegate = self
40
+ mail_controller.setToRecipients(Array(options[:to]))
41
+ mail_controller.setCcRecipients(Array(options[:cc]))
42
+ mail_controller.setBccRecipients(Array(options[:bcc]))
43
+ mail_controller.setSubject(options[:subject] || "Contact")
44
+ is_html = !!options[:html]
45
+ mail_controller.setMessageBody(options[:message], isHTML: is_html)
46
+
47
+ mail_controller
48
+ end
49
+
50
+ # Event when the MFMailComposeViewController is closed
51
+ # -------------------------------------------------------------
52
+ # the callback is fired if it was present in the constructor
53
+
54
+ def mailComposeController(controller, didFinishWithResult: result, error: error)
55
+ @delegate.dismissModalViewControllerAnimated(@mailer_is_animated)
56
+ @callback.call Result.new(result, error) if @callback
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ module BubbleWrap
2
+ module Mail
3
+ class Result
4
+ attr_accessor :result, :error
5
+
6
+ def initialize(result, error)
7
+ self.result = result
8
+ self.error = error
9
+ end
10
+
11
+ def sent?
12
+ self.result == MFMailComposeResultSent
13
+ end
14
+
15
+ def canceled?
16
+ self.result == MFMailComposeResultCancelled
17
+ end
18
+
19
+ def saved?
20
+ self.result == MFMailComposeResultSaved
21
+ end
22
+
23
+ def failed?
24
+ self.result == MFMailComposeResultFailed || self.error
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -19,8 +19,9 @@ module BubbleWrap
19
19
 
20
20
  # Trigger an event
21
21
  def trigger(event, *args)
22
- __events__[event].map do |event|
23
- event.call(*args)
22
+ blks = __events__[event].clone
23
+ blks.map do |blk|
24
+ blk.call(*args)
24
25
  end
25
26
  end
26
27