BubbleWrap for RubyMotion
A collection of (tested) helpers and wrappers used to wrap Cocoa Touch and AppKit code and provide more Ruby like APIs.
Installation
gem install bubble-wrapSetup
- Edit the
Rakefileof your RubyMotion project and add the following require line:
require 'bubble-wrap'If you use Bundler:
gem 'bubble-wrap', '~> 1.9.7'BubbleWrap is split into multiple modules so that you can easily choose which parts are included at compile-time.
If you wish to only include the RSS Parser wrapper:
require 'bubble-wrap/rss_parser'If you wish to only include the Reactor wrapper:
require 'bubble-wrap/reactor'If you wish to only include the UI-related wrappers:
require 'bubble-wrap/ui'If you wish to only include the Camera wrapper:
require 'bubble-wrap/camera'If you wish to only include the Location wrapper:
require 'bubble-wrap/location'If you wish to only include the Media wrapper:
require 'bubble-wrap/media'If you wish to only include the Mail wrapper:
require 'bubble-wrap/mail'If you wish to only include the SMS wrapper:
require 'bubble-wrap/sms'If you wish to only include the Motion (CoreMotion) wrapper:
require 'bubble-wrap/motion'If you wish to only include the NetworkIndicator wrapper:
require 'bubble-wrap/network-indicator'If you want to include everything (ie kitchen sink mode) you can save time and do:
require 'bubble-wrap/all'You can also do this directly in your Gemfile like so:
gem 'bubble-wrap', require: %w[bubble-wrap/core bubble-wrap/location, bubble-wrap/reactor]Note: DON'T use app.files = in your Rakefile to set up your files once you've required BubbleWrap.
Make sure to append onto the array or use +=.
- Now, you can use BubbleWrap extension in your app:
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
puts "#{App.name} (#{App.documents_path})"
true
end
endNote: You can also vendor this repository but the recommended way is to use the versioned gem.
Core
Misc
UUID generator:
BubbleWrap.create_uuid
=> "68ED21DB-82E5-4A56-ABEB-73650C0DB701"Localization (using NSBundle.mainBundle.localizedStringForKey):
BubbleWrap.localized_string(:foo, 'fallback')
=> "fallback"Color conversion:
BubbleWrap.rgba_color(23, 45, 12, 0.4)
=> #<UIDeviceRGBColor:0x6db6ed0>
BubbleWrap.rgb_color(23, 45, 12)
=> #<UIDeviceRGBColor:0x8ca88b0>
'blue'.to_color
=> #<UICachedDeviceRGBColor:0xda535c0>
'dark_gray'.to_color
=> #<UICachedDeviceWhiteColor:0x8bb5be0>
'#FF8A19'.to_color
=> #<UIDeviceRGBColor:0x8d54110>
'#88FF8A19'.to_color # ARGB format
=> #<UIDeviceRGBColor:0xca0fe00>Debug flag:
BubbleWrap.debug?
=> false
BubbleWrap.debug = true
=> true
BubbleWrap.debug?
=> trueApp
A module with useful methods related to the running application
> App.documents_path
# "/Users/mattetti/Library/Application Support/iPhone Simulator/5.0/Applications/EEC6454E-1816-451E-BB9A-EE18222E1A8F/Documents"
> App.resources_path
# "/Users/mattetti/Library/Application Support/iPhone Simulator/5.0/Applications/EEC6454E-1816-451E-BB9A-EE18222E1A8F/testSuite_spec.app"
> App.name
# "testSuite"
> App.identifier
# "io.bubblewrap.testSuite"
> App.alert("BubbleWrap is awesome!")
# creates and shows an alert message.
> App.alert("BubbleWrap is awesome!", {cancel_button_title: "I know it is!", message: "Like, seriously awesome."})
# creates and shows an alert message with optional parameters.
> App.run_after(0.5) { p "It's #{Time.now}" }
# Runs the block after 0.5 seconds.
> App.open_url("http://matt.aimonetti.net")
> App.open_url("tel://123456789")
# Opens the url using the device's browser. Can also open custom URL schemas (accepts a string url or an instance of `NSURL`.)
> App.can_open_url("tel://")
# Returns whether the app can open a given URL resource.
> App::Persistence['channels'] # application specific persistence storage
# ['NBC', 'ABC', 'Fox', 'CBS', 'PBS']
> App::Persistence['channels'] = ['TF1', 'France 2', 'France 3']
# ['TF1', 'France 2', 'France 3']
> App.environment
# 'test'Other available methods:
App.notification_centerApp.user_cacheApp.statesApp.frameApp.delegateApp.sharedApp.windowApp.current_localeApp.release?App.test?App.development?
Device
A collection of useful methods about the current device:
Examples:
> Device.iphone?
# true
> Device.ipad?
# false
> Device.camera.front?
# true
> Device.camera.rear?
# true
> Device.orientation
# :portrait
> Device.interface_orientation
# :portrait
> Device.simulator?
# true
> Device.ios_version
# "6.0"
> Device.retina?
# false
> Device.screen.width
# 320
> Device.screen.height
# 480
> Device.screen.width_for_orientation(:landscape_left)
# 480
> Device.screen.height_for_orientation(:landscape_left)
# 320
> Device.vendor_identifier
# <NSUUID>Camera
Added interface for better camera access:
# Uses the front camera
BW::Device.camera.front.picture(media_types: [:movie, :image]) do |result|
image_view = UIImageView.alloc.initWithImage(result[:original_image])
end
# Uses the rear camera
BW::Device.camera.rear.picture(media_types: [:movie, :image]) do |result|
image_view = UIImageView.alloc.initWithImage(result[:original_image])
end
# Uses the photo library
BW::Device.camera.any.picture(media_types: [:movie, :image]) do |result|
image_view = UIImageView.alloc.initWithImage(result[:original_image])
end
# Lets the user edit the photo (with access to the edited and original photos)
BW::Device.camera.any.picture(allows_editing: true, media_types: [:image]) do |result|
edited_image_view = UIImageView.alloc.initWithImage(result[:edited_image])
original_image_view = UIImageView.alloc.initWithImage(result[:original_image])
end
# Capture a low quality movie with a limit of 10 seconds
BW::Device.camera.front.picture(media_types: [:movie], video_quality: :low, video_maximum_duration: 10) do |result|
video_file_path = result[:media_url]
endOptions include:
-
:allows_editing- Boolean; whether a user can edit the photo/video before picking -
:animated- Boolean; whether to display the camera with an animation (default true) -
:on_dismiss- Lambda; called instead of the default dismissal logic -
:media_types- Array; containing any of[:movie, :image] -
:video_quality- Symbol; one of:high,:medium,low,"640x480".to_sym,iframe1280x720, oriframe960x540. Defaults to:medium -
:video_maximum_duration- Integer; limits movie recording length. Defaults to 600.
JSON
BW::JSON wraps NSJSONSerialization available in iOS5 and offers the same API as Ruby's JSON std lib. For apps building for iOS4, we suggest a different JSON alternative, like AnyJSON.
BW::JSON.generate({'foo' => 1, 'bar' => [1,2,3], 'baz' => 'awesome'})
=> "{\"foo\":1,\"bar\":[1,2,3],\"baz\":\"awesome\"}"
BW::JSON.parse "{\"foo\":1,\"bar\":[1,2,3],\"baz\":\"awesome\"}"
=> {"foo"=>1, "bar"=>[1, 2, 3], "baz"=>"awesome"}NSIndexPath
Helper methods added to give NSIndexPath a bit more of a Ruby
interface.
index_path = table_view.indexPathForCell(cell)
index_path + 1 # NSIndexPath for next cell in the same section
=> #<NSIndexPath:0x120db8e0>NSNotificationCenter
Helper methods to give NSNotificationCenter a Ruby-like interface:
def viewWillAppear(animated)
@foreground_observer = App.notification_center.observe UIApplicationWillEnterForegroundNotification do |notification|
loadAndRefresh
end
@reload_observer = App.notification_center.observe 'ReloadNotification' do |notification|
loadAndRefresh
end
end
def viewWillDisappear(animated)
App.notification_center.unobserve @foreground_observer
App.notification_center.unobserve @reload_observer
end
def reload
App.notification_center.post 'ReloadNotification'
endNSUserDefaults
Helper methods added to the class repsonsible for user preferences used
by the App::Persistence module shown below.
Persistence
Offers a way to persist application specific information using a very simple interface:
> App::Persistence['channels'] # application specific persistence storage
# ['NBC', 'ABC', 'Fox', 'CBS', 'PBS']
> App::Persistence['channels'] = ['TF1', 'France 2', 'France 3']
# ['TF1', 'France 2', 'France 3']
> App::Persistence.delete('channels')
# ['TF1', 'France 2', 'France 3']
> App::Persistence['something__new'] # something previously never stored
# nil
> App::Persistence.all
# {'all':'values', 'stored':'by', 'bubblewrap':'as a hash!'}Observers
Since: > version 0.4
You can observe for object's changes and trigger blocks:
class ExampleViewController < UIViewController
include BW::KVO
def viewDidLoad
@label = UILabel.alloc.initWithFrame [[20,20],[280,44]]
@label.text = ""
view.addSubview @label
observe(@label, :text) do |old_value, new_value|
puts "Hello from viewDidLoad!"
end
end
def viewDidAppear(animated)
observe(@label, :text) do |old_value, new_value|
puts "Hello from viewDidAppear!"
end
end
endYou can remove observers using unobserve method.
Since: > version 1.9.0
Optionally, multiple key paths can be passed to the observer method:
class ExampleViewController < UIViewController
include BW::KVO
def viewDidLoad
@label = UILabel.alloc.initWithFrame [[20,20],[280,44]]
@label.text = ""
view.addSubview @label
observe(@label, [:text, :textColor]) do |old_value, new_value, key_path|
puts "Hello from viewDidLoad for #{key_path}!"
end
end
endAlso you can use observe! method to register observer that will immediately
return initial value. Note that in this case only new value will be passed to
the block.
String
The Ruby String class was extended to add #camelize and
#underscore methods.
> "matt_aimonetti".camelize
=> "MattAimonetti"
> "MattAimonetti".underscore
=> "matt_aimonetti"Time
The Time Ruby class was added a class level method to convert a
iso8601 formatted string into a Time instance.
> Time.iso8601("2012-05-31T19:41:33Z")
=> 2012-05-31 21:41:33 +0200Location
Interface for Ruby-like GPS and compass access (the CoreLocation framework):
> BW::Location.enabled? # Whether location services are enabled on the device
=> true
> BW::Location.authorized? # If your app is authorized to use location services
=> falseBW::Location.get(purpose: 'We need to use your GPS because...') do |result|
p "From Lat #{result[:from].latitude}, Long #{result[:from].longitude}"
p "To Lat #{result[:to].latitude}, Long #{result[:to].longitude}"
endNote: result[:from] will return nil the first time location services are started.
The :previous key in the BW::Location.get() result hash will always return an array of zero or more additional CLLocation objects aside from the locations returned from the :to and :from hash keys. While in most scenarios this array will be empty, per Apple's Documentation if there are deferred updates or multiple locations that arrived before they could be delivered, multiple locations will be returned in an order of oldest to newest.
BW::Location.get_compass do |result|
p result[:magnetic_heading] # Heading towards magnetic north
p result[:true_heading] # Heading towards true north
p result[:accuracy] # Potential error between magnetic and true heading
p result[:timestamp] # Timestamp of the heading calculation
endBW::Location.get_significant is also available, for monitoring significant location changes.
BW::Location also supports get_once-style methods, which will return the first result before ending the search:
BW::Location.get_once(desired_accuracy: :three_kilometers, ...) do |result|
if result.is_a?(CLLocation)
p result.coordinate.latitude
p result.coordinate.longitude
else
p "ERROR: #{result[:error]}"
end
end
BW::Location.get_compass_once do |heading|
p result[:magnetic_heading]
p result[:true_heading]
p result[:accuracy]
p result[:timestamp]
endiOS 8 Location Requirements
iOS 8 introduced stricter location services requirements. Although BubbleWrap will handle most of this for you automatically, you are required to add a few key/value pairs to the Info.plist. Add these two lines to your Rakefile (with your descriptions, obviously):
app.info_plist['NSLocationAlwaysUsageDescription'] = 'Description'
app.info_plist['NSLocationWhenInUseUsageDescription'] = 'Description'Note: you need both keys to use get_once, so it's probably best to just include both no matter what. See Apple's documentation on iOS 8 location services requirements for more information.
Motion
Interface for the accelerometer, gyroscope, and magnetometer sensors (the
CoreMotion framework). You can access each sensor individually, or you can get
data from all of them at once using the BW::Motion.device interface, which
delegates to CMMotionManager#deviceMotion.
Each sensor has an every and once method. every expects a time interval,
and you will need to retain the object it returns and call #stop on it when
you are done with the data.
The every and once methods can accept a :queue option. The default value
is a queue that runs on the main loop, so that UI updates can be processed in
the block. This is useful, but not recommended by Apple, since the events can
come in at a high rate. If you want to use a background queue, you can either
specify an NSOperationQueue object, or you can use one of these symbols:
-
:main-NSOperationQueue.mainQueue, this is the default value. -
:background- BubbleWrap will create a newNSOperationQueue. -
:current- BubbleWrap will useNSOperationQueue.currentQueue.
If you pass a string instead, a new queue will be created and its name
property will be set to that string.
The CMDeviceMotion interface (BW::Motion.device) accepts a :reference
option, which specifies the CMAttitudeReferenceFrame. The default value is
the same as the one that CMMotionManager uses, which is returned by the
CMMotionManager#attitudeReferenceFrame method. This option should be passed
to the repeat, every or once methods.
Accelerometer
BW::Motion.accelerometer.available?
BW::Motion.accelerometer.data # returns CMAccelerometerData object or nil
# ask the CMMotionManager to update every 5 seconds
BW::Motion.accelerometer.every(5) do |result|
# result contains the following data (from CMAccelerometerData#acceleration):
p result[:data] # the CMAccelerometerData object
p result[:acceleration] # the CMAcceleration struct
p result[:x] # acceleration in the x direction
p result[:y] # " y direction
p result[:z] # " z direction
end
# every, start, and repeat all need to be stopped later.
BW::Motion.accelerometer.stop
# repeat, but don't set the interval
BW::Motion.accelerometer.repeat do |result|
end
# you can specify a :queue where the operations will be executed. See above for details
BW::Motion.accelerometer.every(5, queue: :background) { |result| ... }
BW::Motion.accelerometer.every(5, queue: :main) { |result| ... }
BW::Motion.accelerometer.every(5, queue: :current) { |result| ... }
BW::Motion.accelerometer.every(5, queue: 'my queue') { |result| ... }
BW::Motion.accelerometer.once do |result|
# ...
endGyroscope
BW::Motion.gyroscope.available?
BW::Motion.gyroscope.data # returns CMGyroData object or nil
# ask the CMMotionManager to update every second.
BW::Motion.gyroscope.every(1) do |result|
# result contains the following data (from CMGyroData#rotationRate):
p result[:data] # the CMGyroData object
p result[:rotation] # the CMRotationRate struct
p result[:x] # rotation in the x direction
p result[:y] # " y direction
p result[:z] # " z direction
end
BW::Motion.gyroscope.stop
BW::Motion.gyroscope.once do |result|
# ...
endMagnetometer
BW::Motion.magnetometer.available?
BW::Motion.magnetometer.data # returns CMMagnetometerData object or nil
# ask the CMMotionManager to update every second
BW::Motion.magnetometer.every(1) do |result|
# result contains the following data (from CMMagnetometerData#magneticField):
p result[:data] # the CMMagnetometerData object
p result[:field] # the CMMagneticField struct
p result[:x] # magnetic field in the x direction
p result[:y] # " y direction
p result[:z] # " z direction
end
BW::Motion.magnetometer.stop
BW::Motion.magnetometer.once do |result|
# ...
endDevice Motion
This is an amalgam of all the motion sensor data.
BW::Motion.device.available?
BW::Motion.device.data # returns CMDeviceMotion object or nil
BW::Motion.device.every(1) do |result|
# result contains the following data:
p result[:data] # the CMDeviceMotion object
# orientation data, from CMDeviceMotion#attitude
p result[:attitude] # the CMAttitude struct
p result[:roll]
p result[:pitch]
p result[:yaw]
# rotation data, from CMDeviceMotion#rotationRate
p result[:rotation] # the CMRotationRate struct
p result[:rotation_x]
p result[:rotation_y]
p result[:rotation_z]
# gravity+acceleration vector, from CMDeviceMotion#gravity
p result[:gravity] # the CMAcceleration struct
p result[:gravity_x]
p result[:gravity_y]
p result[:gravity_z]
# just the acceleration vector, from CMDeviceMotion#userAcceleration
p result[:acceleration] # the CMAcceleration struct
p result[:acceleration_x]
p result[:acceleration_y]
p result[:acceleration_z]
# the magnetic data, from CMDeviceMotion#magneticField
p result[:magnetic] # the CMCalibratedMagneticField struct
p result[:magnetic_field] # the CMMagneticField struct from the CMCalibratedMagneticField
p result[:magnetic_x]
p result[:magnetic_y]
p result[:magnetic_z]
p result[:magnetic_accuracy] # this will be a symbol, :low, :medium, :high, or nil if the magnetic data is uncalibrated
# less useful data from CMAttitude, unless you're into the whole linear algebra thing:
p result[:matrix] # CMAttitude#rotationMatrix
p result[:quarternion] # CMAttitude#quarternion
end
# the reference frame should be one of the CMAttitudeReferenceFrame constants...
ref = CMAttitudeReferenceFrameXArbitraryZVertical
# ... or one of these symbols: :arbitrary_z, :corrected_z, :magnetic_north, :true_north
ref = :corrected_z
BW::Motion.device.every(1, queue: :background, reference: ref) { |result| ... }
BW::Motion.device.once do |result|
# ...
endMedia
Added wrapper for playing remote and local media. Available are modal and custom presentation styles:
# Plays in your custom frame
local_file = NSURL.fileURLWithPath(File.join(NSBundle.mainBundle.resourcePath, 'test.mp3'))
BW::Media.play(local_file) do |media_player|
media_player.view.frame = [[10, 100], [100, 100]]
self.view.addSubview media_player.view
end
# Plays in an independent modal controller
BW::Media.play_modal("http://www.hrupin.com/wp-content/uploads/mp3/testsong_20_sec.mp3")Wrapper for showing an in-app mail composer view.
You should always determine if the device your app is running on is configured to send mail before displaying a mail composer window. BW::Mail.can_send_mail? will return true or false.
# Opens as a modal in the current UIViewController
BW::Mail.compose(
delegate: self, # optional, defaults to rootViewController
to: [ "tom@example.com" ],
cc: [ "itchy@example.com", "scratchy@example.com" ],
bcc: [ "jerry@example.com" ],
html: false,
subject: "My Subject",
message: "This is my message. It isn't very long.",
animated: false
) do |result, error|
result.sent? # => boolean
result.canceled? # => boolean
result.saved? # => boolean
result.failed? # => boolean
error # => NSError
endSMS
Wrapper for showing an in-app message (SMS) composer view.
You should always determine if the device your app is running on can send SMS messages before displaying a SMS composer window. BW::SMS.can_send_sms? will return true or false.
# Opens as a modal in the current UIViewController
BW::SMS.compose (
{
delegate: self, # optional, will use root view controller by default
to: [ "1(234)567-8910" ],
message: "This is my message. It isn't very long.",
animated: false
}) {|result, error|
result.sent? # => boolean
result.canceled? # => boolean
result.failed? # => boolean
error # => NSError
}NetworkIndicator
Wrapper for showing and hiding the network indicator (the status bar spinner).
BW::NetworkIndicator.show # starts the spinner
BW::NetworkIndicator.hide # stops it
# the nice thing is if you call 'show' multiple times, the 'hide' method will
# not have any effect until you've called it the same number of times.
BW::NetworkIndicator.show
# ...somewhere else
BW::NetworkIndicator.show
# ...down the line
BW::NetworkIndicator.hide
# indicator is still visible
BW::NetworkIndicator.hide
# NOW the indicator is hidden!
# If you *really* want to hide the indicator immediately, you can call `reset!`
# but this is in no way encouraged.
BW::NetworkIndicator.reset!
# and for completeness, a check to see if the indicator is visible
BW::NetworkIndicator.visible?UI
Gestures
Extra methods on UIView for working with gesture recognizers. A gesture recognizer can be added using a normal Ruby block, like so:
view.when_tapped do
UIView.animateWithDuration(1,
animations:lambda {
# animate
# @view.transform = ...
})
endThere 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.
In order to prevent retain cycles due to strong references within the passed block, use the use_weak_callbacks flag so the blocks do not retain a strong reference to self:
BubbleWrap.use_weak_callbacks = trueUIViewController
A custom method was added to UIViewController to return the content
frame of a view controller.
UIControl / UIButton
Helper methods to give UIButton a Ruby-like interface. Ex:
button.when(UIControlEventTouchUpInside) do
self.view.backgroundColor = UIColor.redColor
endThe #when method also accepts bitwise combinations of events:
button.when(UIControlEventTouchUpInside | UIControlEventTouchUpOutside) do
self.view.backgroundColor = UIColor.redColor
endYou can use symbols for events (but won't work with the bitwise operator):
button.when(:touch_up_inside) do
self.view.backgroundColor = UIColor.redColor
end
button.when(:value_changed) do
self.view.backgroundColor = UIColor.blueColor
endSet the use_weak_callbacks flag so the blocks do not retain a strong reference to self:
BubbleWrap.use_weak_callbacks = trueUIBarButtonItem
BW::UIBarButtonItem is a subclass of UIBarButtonItem with an natural Ruby syntax.
Constructors
Instead specifying a target-action pair, each constructor method accepts an optional block. When the button is tapped, the block is executed.
BW::UIBarButtonItem.system(:save) do
# ...
end
title = "Friends"
BW::UIBarButtonItem.styled(:plain, title) do
# ...
end
image = UIImage.alloc.init
BW::UIBarButtonItem.styled(:bordered, image) do
# ...
end
image = UIImage.alloc.init
landscape = UIImage.alloc.init
BW::UIBarButtonItem.styled(:bordered, image, landscape) do
# ...
end
view = UIView.alloc.init
BW::UIBarButtonItem.custom(view) do
# ...
end
# NOTE: The block is attached to the view as a single tap gesture recognizer.The .new constructor provides a flexible, builder-style syntax.
options = { :system => :save }
BW::UIBarButtonItem.new(options) do
# ...
end
options = { :styled => :plain, :title => "Friends" }
BW::UIBarButtonItem.new(options) do
# ...
end
options = { :styled => :bordered, :image => UIImage.alloc.init }
BW::UIBarButtonItem.new(options) do
# ...
end
options = {
:styled => :bordered,
:image => UIImage.alloc.init,
:landscape => UIImage.alloc.init
}
BW::UIBarButtonItem.new(options) do
# ...
end
options = { :custom => UIView.alloc.init }
BW::UIBarButtonItem.new(options) do
# ...
end
# NOTE: The block is attached to the view as a single tap gesture recognizer.Button types
The .styled button types are:
:plain
:bordered
:doneAnd the .system button types are:
:done
:cancel
:edit
:save
:add
:flexible_space
:fixed_space
:compose
:reply
:action
:organize
:bookmarks
:search
:refresh
:stop
:camera
:trash
:play
:pause
:rewind
:fast_forward
:undo
:redo
:page_curlUIActivityViewController
BW::UIActivityViewController is a subclass of UIActivityViewController with an natural Ruby syntax.
You can initiate a UIActivityViewController with or without a completion handler block. For more information on UIActivityViewControllers, see Apple's documentation.
# Without a completion handler
BW::UIActivityViewController.new(
items: "Some Text", # or ["Some Text", NSURL.URLWithString('http://www.rubymotion.com')] or a UIImage
animated: true, # Defaults to true
excluded: :add_to_reading_list # One item or an array
)
# With completion handler
BW::UIActivityViewController.new(
items: "Some Text",
animated: true,
excluded: [:add_to_reading_list, :print, :air_drop]
) do |activity_type, completed|
puts "completed with activity: #{activity_type} - finished?: #{completed}"
endBuilt in activities that can be passed to the excluded option are defined as UIActivity class UIActivityType constants:
:post_to_facebook
:post_to_twitter
:post_to_weibo
:message
:mail
:print
:copy_to_pasteboard
:assign_to_contact
:save_to_camera_roll
:add_to_reading_list
:post_to_flickr
:post_to_vimeo
:post_to_tencent_weibo
:air_dropRSS Parser
Since: > version 1.0.0
The RSS Parser provides an easy interface to consume RSS feeds in an asynchronous (non blocking) way.
feed_parser = BW::RSSParser.new("http://feeds2.feedburner.com/sdrbpodcast")
feed_parser.parse do |item|
# called asynchronously as items get parsed
p item.title
endThe yielded RSS item is of type RSSParser::RSSItem and has the
following attributes:
- title
- description
- link
- guid
- pubDate
- enclosure
The item can be converted into a hash by calling to_hash on it.
Delegate
Since: > version 1.0.0
You can also designate a delegate to the parser and implement change state callbacks:
feed_parser = BW::RSSParser.new("http://feeds.feedburner.com/sdrbpodcast")
feed_parser.delegate = self
feed_parser.parse do |item|
p item.title
end
# Delegate method
def when_parser_initializes
p "The parser is ready!"
end
def when_parser_parses
p "The parser started parsing the document"
end
def when_parser_is_done
p "The feed is entirely parsed, congratulations!"
end
def when_parser_errors
p "The parser encountered an error"
ns_error = feed_parser.parserError
p ns_error.localizedDescription
endThese delegate methods are optional, however, you might find the
when_parser_is_done callback useful if you collected all the items and
want to process all at once for instance.
Parsing a remote content or actual data
You have the choice to initialize a parser instance with a string
representing an URL, an instance of NSURL or my specifying that the
passed param is some data to parse directly.
# string representing an url:
feed_parser = BW::RSSParser.new("http://feeds2.feedburner.com/sdrbpodcast")
# a NSURL instance:
url = NSURL.alloc.initWithString("http://matt.aimonetti.net/atom.xml")
feed_parser = BW::RSSParser.new(url)
# Some data
feed = File.read('atom.xml')
feed_parser = BW::RSSParser.new(feed, true)Reactor
Since: > version 1.0.0
BW::Reactor is a simplified, mostly complete implementation of
the Event Machine API. In fact
BW::Reactor is aliased to EM in the runtime environment.
Deferables
BubbleWrap provides both a Deferrable mixin and a DefaultDeferrable
class, which simply mixes in deferrable behaviour if you don't want to
implement your own.
A deferrable is an object with four states: unknown, successful, failure and timeout. When you initially create a deferrable it is in an unknown state, however you can assign callbacks to be run when the object changes to either successful or failure state.
Using delegate, errback_delegate and callback_delegate you can link
deferrables together.
By default, callbacks will be made on the thread that the deferrable succeeds/fails on. For multithreaded environments, it can be useful to use EM::ThreadAwareDeferrable so that callbacks will be made on the threads they are declared on.
Success
> d = EM::DefaultDeferrable.new
=> #<BW::Reactor::DefaultDeferrable:0x6d859a0>
> d.callback { |what| puts "Great #{what}!" }
=> [#<Proc:0x6d8a1e0>]
> d.succeed "justice"
Great justice!
=> nilFailure
> d = EM::DefaultDeferrable.new
=> #<BW::Reactor::DefaultDeferrable:0x8bf3ee0>
> d.errback { |what| puts "Great #{what}!" }
=> [#<Proc:0x8bf3ef0>]
> d.fail "sadness"
Great sadness!
=> nilDelegate
> d = EM::DefaultDeferrable.new
=> #<BW::Reactor::DefaultDeferrable:0x8bf3ee0>
> delegate = EM::DefaultDeferrable.new
=> #<BW::Reactor::DefaultDeferrable:0x8bf5910>
> d.delegate delegate
=> #<BW::Reactor::DefaultDeferrable:0x8bf3ee0>
> delegate.callback { |*args| puts args }
=> [#<Proc:0x8bf3ef0>]
> d.succeed :passed
=> nil
=> [:passed]DependentDeferrable
DependentDeferrable depends on children deferrables. A DependentDeferrable
succeeds only when every child succeeds and fails immediately when any child
fails
> d1 = EM::DefaultDeferrable.new
=> #<BubbleWrap::Reactor::DefaultDeferrable:0x10c713750>
> d2 = EM::DefaultDeferrable.new
=> #<BubbleWrap::Reactor::DefaultDeferrable:0x10370bb10>
> d = EM::DependentDeferrable.on(d1, d2)
=> #<BubbleWrap::Reactor::DependentDeferrable:0x106c17b80>
> d.callback {|a, b| puts "a: #{a} b: #{b}"}
=> [#<Proc:0x103075210>]
> d1.succeed 'one', 'one more'
> d2.succeed :two
a: ["one", "one more"] b: [:two]ThreadAwareDeferrable
> d = EM::ThreadAwareDeferrable.new
=> #<BW::Reactor::ThreadAwareDeferrable:0x8bf3ee0>
> queue = Dispatch::Queue.new(:deferrable.to_s)
> queue.async do
> d.callback do |*args|
> Dispatch::Queue.current == queue
> => true # this is normally false
> end
> end
> d.succeed trueTimeout
> d = EM::DefaultDeferrable.new
=> #<BW::Reactor::DefaultDeferrable:0x8bf5910>
> d.errback { puts "Great scott!" }
=> [#<Proc:0x8bf6350>]
> d.timeout 2
=> #<BW::Reactor::Timer:0x6d920a0 @timer=#<__NSCFTimer:0x6d91990>>
# wait...
> Great scott!Timers
All timers can be cancelled using EM.cancel_timer.
One-shot timers
> EM.add_timer 1.0 do
> puts "Great scott!"
> end
=> 146335904
> Great scott!Periodic timers
> count = 0
=> 0
> timer = EM.add_periodic_timer 1.0 do
> count = count + 1
> puts "Great scott!"
> (count < 10) || EM.cancel_timer(timer)
> end
=> 146046832
> Great scott!
Great scott!
Great scott!
Great scott!
Great scott!
Great scott!
Great scott!
Great scott!
Great scott!
Great scott!Scheduling operations
You can use EM.schedule to schedule blocks to be executed
asynchronously. BubbleWrap deviates from the EventMachine
API here in that it also provides EM.schedule_on_main which
makes sure that the task is run asynchronously, but on the
application's main thread - this is necessary if you are
updating the user interface.
> EM.schedule { puts Thread.current.object_id }
146027920
=> nil
> EM.schedule_on_main { puts Thread.current.object_id }
112222480
=> nilDeferrable operations
You can also use EM.defer in much the same way as EM.schedule
with one important difference, you can pass in a second proc
which will be called when the first has completed, and be passed
it's result as an argument. Just like EM.schedule, EM.defer
also has an EM.defer_on_main version.
> operation = proc { 88 }
=> #<Proc:0x6d763c0>
> callback = proc { |speed| puts speed >= 88 ? "Time travel!" : "Conventional travel!" }
=> #<Proc:0x8bd3910>
> EM.defer(operation, callback)
=> nil
Time travel!Events
Although not part of the EventMachine API, BubbleWrap provides
an Eventable mixin for use instrumenting objects with simple
event triggering behaviour. BW::Reactor uses this
behind the scenes in several places, and as it's a very handy
idiom it is available as a public API.
> o = Class.new { include EM::Eventable }.new
=> #<#<Class:0xab63f00>:0xab64430>
> o.on(:november_5_1955) { puts "Ow!" }
=> [#<Proc:0xad9bf00>]
> flux = proc{ puts "Flux capacitor!" }
=> #<Proc:0xab630f0>
> o.on(:november_5_1955, &flux)
=> [#<Proc:0xad9bf00>, #<Proc:0xab630f0>]
> o.trigger(:november_5_1955)
Ow!
Flux capacitor!
=> [nil, nil]
> o.off(:november_5_1955, &flux)
=> #<Proc:0xab630f0>
> o.trigger(:november_5_1955)
Ow!
=> [nil]
> o.on(:november_5_1955) { puts "Ow!" }
> o.on(:november_5_1955) { puts "Another Ow!" }
> o.off(:november_5_1955)
=> nilContributing
Do you have a suggestion for a specific wrapper? Feel free to open an issue/ticket and tell us about what you are after. If you have a wrapper/helper you are using and are thinking that others might enjoy, please send a pull request with tests. If you need help writing the tests, send the pull request anyways and we'll try to help you out with that.
- Create an issue in GitHub to make sure your PR will be accepted
- Fork the BubbleWrap repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Write tests for your changes and ensure they pass locally (
bundle exec rake spec && bundle exec rake spec osx=true) - Push to the branch (
git push origin my-new-feature) - Create new Pull Request