├── .yardopts ├── resources ├── Localizable.strings └── test.mp3 ├── lib ├── bubble-wrap │ ├── ext.rb │ ├── font.rb │ ├── rss_parser.rb │ ├── version.rb │ ├── network-indicator.rb │ ├── all.rb │ ├── sms.rb │ ├── mail.rb │ ├── test.rb │ ├── ui.rb │ ├── location.rb │ ├── http.rb │ ├── reactor.rb │ ├── camera.rb │ ├── media.rb │ ├── core.rb │ ├── ext │ │ └── motion_project_app.rb │ ├── requirement │ │ └── path_manipulation.rb │ ├── loader.rb │ └── requirement.rb └── bubble-wrap.rb ├── samples ├── gesture │ ├── app │ │ ├── helpers │ │ │ └── drawing_helper.rb │ │ ├── controllers │ │ │ └── drawing_view_controller.rb │ │ ├── app_delegate.rb │ │ └── views │ │ │ └── drawing │ │ │ ├── rect_view.rb │ │ │ └── gesture_view.rb │ ├── .gitignore │ ├── Gemfile │ ├── README.md │ ├── spec │ │ └── main_spec.rb │ ├── Gemfile.lock │ └── Rakefile ├── camera │ ├── Gemfile │ ├── README.md │ ├── spec │ │ └── main_spec.rb │ ├── Rakefile │ └── app │ │ ├── app_delegate.rb │ │ └── controllers │ │ └── camera_controller.rb ├── location │ ├── Gemfile │ ├── README.md │ ├── .gitignore │ ├── spec │ │ └── main_spec.rb │ ├── Rakefile │ └── app │ │ ├── app_delegate.rb │ │ ├── models │ │ └── places.rb │ │ └── controllers │ │ └── image_list_controller.rb ├── alert │ ├── Gemfile │ ├── resources │ │ └── Default-568h@2x.png │ ├── spec │ │ └── main_spec.rb │ ├── .gitignore │ ├── Rakefile │ └── app │ │ ├── app_delegate.rb │ │ └── controllers │ │ └── alert_view_controller.rb ├── osx │ ├── Gemfile │ ├── spec │ │ └── main_spec.rb │ ├── Gemfile.lock │ ├── Rakefile │ ├── resources │ │ └── Credits.rtf │ └── app │ │ ├── app_delegate.rb │ │ └── menu.rb └── media │ ├── resources │ ├── test.mp3 │ └── Default-568h@2x.png │ ├── spec │ └── main_spec.rb │ ├── .gitignore │ ├── Rakefile │ └── app │ ├── app_delegate.rb │ └── controllers │ └── play_controller.rb ├── .gitignore ├── motion ├── core │ ├── osx │ │ ├── device.rb │ │ └── app.rb │ ├── device │ │ ├── screen.rb │ │ ├── osx │ │ │ └── screen.rb │ │ └── ios │ │ │ ├── camera_wrapper.rb │ │ │ └── screen.rb │ ├── pollute.rb │ ├── device.rb │ ├── ns_url_request.rb │ ├── ns_user_defaults.rb │ ├── ns_index_path.rb │ ├── ns_notification_center.rb │ ├── time.rb │ ├── json.rb │ ├── persistence.rb │ ├── ios │ │ ├── device.rb │ │ └── app.rb │ ├── app.rb │ ├── string.rb │ └── kvo.rb ├── location │ └── pollute.rb ├── reactor │ ├── default_deferrable.rb │ ├── future.rb │ ├── timer.rb │ ├── eventable.rb │ ├── periodic_timer.rb │ └── queue.rb ├── ui │ ├── pollute.rb │ ├── ui_view_controller_wrapper.rb │ ├── ui_control_wrapper.rb │ ├── ui_activity_view_controller_wrapper.rb │ ├── ui_view_wrapper.rb │ └── ui_bar_button_item.rb ├── media │ └── media.rb ├── sms │ ├── result.rb │ └── sms.rb ├── mail │ ├── result.rb │ └── mail.rb ├── shortcut.rb ├── core.rb ├── util │ ├── deprecated.rb │ └── constants.rb ├── network-indicator │ └── network-indicator.rb ├── font │ └── font.rb ├── test_suite_delegate.rb ├── reactor.rb └── rss_parser.rb ├── Gemfile ├── spec ├── lib │ ├── motion_stub.rb │ ├── bubble-wrap_spec.rb │ └── bubble-wrap │ │ ├── ext │ │ ├── motion_project_config_spec.rb │ │ └── motion_project_app_spec.rb │ │ ├── requirement │ │ └── path_manipulation_spec.rb │ │ └── requirement_spec.rb └── motion │ ├── core │ ├── device_spec.rb │ ├── osx │ │ └── app_spec.rb │ ├── ns_index_path_spec.rb │ ├── device │ │ ├── osx │ │ │ └── screen_spec.rb │ │ └── ios │ │ │ ├── device_spec.rb │ │ │ ├── camera_wrapper_spec.rb │ │ │ └── camera_spec.rb │ ├── ns_notification_center_spec.rb │ ├── time_spec.rb │ ├── app_spec.rb │ ├── persistence_spec.rb │ └── json_spec.rb │ ├── ui │ ├── pollute_spec.rb │ ├── ui_view_controller_wrapper_spec.rb │ ├── ui_activity_view_controller_wrapper_spec.rb │ ├── ui_control_wrapper_spec.rb │ └── ui_view_wrapper_spec.rb │ ├── sms │ ├── result_spec.rb │ └── sms_spec.rb │ ├── util │ ├── constants_spec.rb │ └── deprecated_spec.rb │ ├── mail │ ├── result_spec.rb │ └── mail_spec.rb │ ├── font │ └── font_spec.rb │ ├── core_spec.rb │ ├── rss_parser_spec.rb │ ├── media │ └── player_spec.rb │ ├── network-indicator │ └── network_indicator_spec.rb │ └── reactor_spec.rb ├── .travis.yml ├── Gemfile.lock ├── Rakefile ├── LICENSE ├── bubble-wrap.gemspec ├── GEM.md ├── GETTING_STARTED.md └── HACKING.md /.yardopts: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | motion/**/*.rb -------------------------------------------------------------------------------- /resources/Localizable.strings: -------------------------------------------------------------------------------- 1 | "real_key" = "Real Key"; -------------------------------------------------------------------------------- /lib/bubble-wrap/ext.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/ext/motion_project_app' 2 | -------------------------------------------------------------------------------- /samples/gesture/app/helpers/drawing_helper.rb: -------------------------------------------------------------------------------- 1 | module DrawingHelper 2 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rake_tasks~ 2 | pkg/* 3 | build/ 4 | .DS_Store 5 | .repl_history 6 | 0 7 | -------------------------------------------------------------------------------- /resources/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/BubbleWrap/master/resources/test.mp3 -------------------------------------------------------------------------------- /samples/camera/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bubble-wrap', '~> 1.1.0' 4 | -------------------------------------------------------------------------------- /samples/location/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bubble-wrap', '~> 1.1.0' 4 | -------------------------------------------------------------------------------- /samples/alert/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "bubble-wrap", :path => "../../" 4 | -------------------------------------------------------------------------------- /samples/camera/README.md: -------------------------------------------------------------------------------- 1 | ## Camera Demo 2 | 3 | A bubble-wrap demo to show how to use camera API. 4 | 5 | -------------------------------------------------------------------------------- /motion/core/osx/device.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module_function 4 | 5 | end 6 | end -------------------------------------------------------------------------------- /samples/location/README.md: -------------------------------------------------------------------------------- 1 | ## Location Demo 2 | 3 | A bubble-wrap demo to show how to use location API. 4 | 5 | -------------------------------------------------------------------------------- /samples/osx/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bubble-wrap', :path => "../../" # '~> 1.3.0' 4 | -------------------------------------------------------------------------------- /samples/media/resources/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/BubbleWrap/master/samples/media/resources/test.mp3 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in bubble-wrap.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /motion/core/device/screen.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module Screen 4 | 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /samples/gesture/.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | resources/*.nib 4 | resources/*.momd 5 | resources/*.storyboardc 6 | -------------------------------------------------------------------------------- /samples/gesture/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'bubble-wrap', '~> 1.1.0' 4 | gem 'motion-dryer' 5 | -------------------------------------------------------------------------------- /samples/location/.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | resources/*.nib 4 | resources/*.momd 5 | resources/*.storyboardc 6 | -------------------------------------------------------------------------------- /samples/alert/resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/BubbleWrap/master/samples/alert/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /samples/media/resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/BubbleWrap/master/samples/media/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /motion/core/pollute.rb: -------------------------------------------------------------------------------- 1 | [ 2 | [NSIndexPath, NSIndexPathWrap] 3 | ].each do |base, wrapper| 4 | base.send(:include, wrapper) 5 | end 6 | -------------------------------------------------------------------------------- /lib/bubble-wrap/font.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | BubbleWrap.require_ios("font") do 3 | BubbleWrap.require('motion/font/font.rb') 4 | end -------------------------------------------------------------------------------- /lib/bubble-wrap/rss_parser.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | BubbleWrap.require('motion/rss_parser.rb') 3 | BubbleWrap.require('motion/http.rb') 4 | -------------------------------------------------------------------------------- /lib/bubble-wrap/version.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | VERSION = '1.6.0' unless defined?(BubbleWrap::VERSION) 3 | MIN_MOTION_VERSION = '2.17' 4 | end 5 | -------------------------------------------------------------------------------- /motion/location/pollute.rb: -------------------------------------------------------------------------------- 1 | [ 2 | [CLLocation, BubbleWrap::CLLocationWrap], 3 | ].each do |base, wrapper| 4 | base.send(:include, wrapper) 5 | end 6 | -------------------------------------------------------------------------------- /lib/bubble-wrap.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/version' unless defined?(BubbleWrap::VERSION) 2 | require File.expand_path('../bubble-wrap/core', __FILE__) 3 | require File.expand_path('../bubble-wrap/http', __FILE__) 4 | -------------------------------------------------------------------------------- /samples/osx/spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'osx'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bubble-wrap/network-indicator.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("network-indicator") do 4 | BubbleWrap.require('motion/core/app.rb') 5 | BubbleWrap.require('motion/network-indicator/**/*.rb') 6 | end 7 | -------------------------------------------------------------------------------- /samples/alert/spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'alert'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/gesture/README.md: -------------------------------------------------------------------------------- 1 | ## Gesture Demo 2 | 3 | A bubble-wrap demo to show how to use gesture API. 4 | 5 | ![](http://f.cl.ly/items/0e1J3W0Y0p3n3S2a0k2K/%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202012-07-16%20%E4%B8%8B%E5%8D%8802.15.30.png) -------------------------------------------------------------------------------- /samples/media/spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'media'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/osx/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../ 3 | specs: 4 | bubble-wrap (1.4.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | bubble-wrap! 15 | -------------------------------------------------------------------------------- /samples/gesture/spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'gesture'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/camera/spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'google_location'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/gesture/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | bubble-wrap (1.1.1) 5 | motion-dryer (0.0.1) 6 | 7 | PLATFORMS 8 | ruby 9 | 10 | DEPENDENCIES 11 | bubble-wrap (~> 1.1.0) 12 | motion-dryer 13 | -------------------------------------------------------------------------------- /samples/location/spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'google_location'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bubble-wrap/all.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../loader', __FILE__) 2 | ['core', 'http', 'reactor', 'rss_parser', 'ui', 'location', 'media', 'font', 'mail','sms', 'network-indicator'].each { |sub| 3 | require File.expand_path("../#{sub}", __FILE__) 4 | } 5 | -------------------------------------------------------------------------------- /samples/gesture/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project' 4 | require 'bundler' 5 | Bundler.require :default 6 | 7 | Motion::Project::App.setup do |app| 8 | app.name = 'gesture' 9 | end 10 | -------------------------------------------------------------------------------- /samples/alert/.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | tags 4 | app/pixate_code.rb 5 | resources/*.nib 6 | resources/*.momd 7 | resources/*.storyboardc 8 | .DS_Store 9 | nbproject 10 | .redcar 11 | #*# 12 | *~ 13 | *.sw[po] 14 | .eprj 15 | .sass-cache 16 | .idea 17 | -------------------------------------------------------------------------------- /samples/camera/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project' 4 | require 'bubble-wrap/core' 5 | require 'bubble-wrap/camera' 6 | 7 | Motion::Project::App.setup do |app| 8 | app.name = 'camera' 9 | end 10 | -------------------------------------------------------------------------------- /samples/media/.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | tags 4 | app/pixate_code.rb 5 | resources/*.nib 6 | resources/*.momd 7 | resources/*.storyboardc 8 | .DS_Store 9 | nbproject 10 | .redcar 11 | #*# 12 | *~ 13 | *.sw[po] 14 | .eprj 15 | .sass-cache 16 | .idea 17 | -------------------------------------------------------------------------------- /samples/alert/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project' 4 | require 'bundler' 5 | Bundler.require 6 | 7 | Motion::Project::App.setup do |app| 8 | # Use `rake config' to see complete project settings. 9 | app.name = 'alert' 10 | end 11 | -------------------------------------------------------------------------------- /samples/location/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project' 4 | require 'bubble-wrap/location' 5 | require 'bubble-wrap/http' 6 | require 'bubble-wrap/core' 7 | 8 | Motion::Project::App.setup do |app| 9 | app.name = 'location' 10 | end 11 | -------------------------------------------------------------------------------- /motion/core/osx/app.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module App 3 | module_function 4 | 5 | # Application Delegate 6 | def delegate 7 | shared.delegate 8 | end 9 | 10 | # the Application object. 11 | def shared 12 | NSApplication.sharedApplication 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /motion/reactor/default_deferrable.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | # A basic class which includes Deferrable when all 4 | # you need is a deferrable without any added behaviour. 5 | class DefaultDeferrable 6 | include ::BubbleWrap::Reactor::Deferrable 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/lib/motion_stub.rb: -------------------------------------------------------------------------------- 1 | # Create a fake Motion class hierarchy for testing. 2 | module Motion 3 | module Project 4 | class App 5 | def self.setup 6 | end 7 | end 8 | 9 | class Config 10 | def files_dependencies 11 | end 12 | end 13 | end 14 | Version = "1.24" 15 | end 16 | -------------------------------------------------------------------------------- /motion/ui/pollute.rb: -------------------------------------------------------------------------------- 1 | # Please, no more! It'll hurt BubbleWrap's compatibility with other libraries. 2 | [ 3 | [UIControl, BW::UIControlWrapper], 4 | [UIView, BW::UIViewWrapper], 5 | [UIViewController, BW::UIViewControllerWrapper], 6 | ].each do |base, wrapper| 7 | base.send(:include, wrapper) 8 | end 9 | -------------------------------------------------------------------------------- /samples/media/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project' 4 | 5 | $:.unshift("../../lib") 6 | require 'bubble-wrap/media' 7 | 8 | Motion::Project::App.setup do |app| 9 | # Use `rake config' to see complete project settings. 10 | app.name = 'media' 11 | end 12 | -------------------------------------------------------------------------------- /samples/osx/Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project/template/osx' 4 | 5 | require 'bundler' 6 | Bundler.require :default 7 | 8 | Motion::Project::App.setup do |app| 9 | # Use `rake config' to see complete project settings. 10 | app.name = 'osx' 11 | end 12 | -------------------------------------------------------------------------------- /samples/alert/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 4 | @window.rootViewController = AlertViewController.alloc.init 5 | @window.makeKeyAndVisible 6 | true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/bubble-wrap/sms.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("sms") do 4 | BubbleWrap.require('motion/core/app.rb') 5 | BubbleWrap.require('motion/sms/**/*.rb') do 6 | file('motion/sms/sms.rb').depends_on('motion/sms/result.rb') 7 | file('motion/sms/sms.rb').uses_framework('MessageUI') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/bubble-wrap/mail.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("mail") do 4 | BubbleWrap.require('motion/core/app.rb') 5 | BubbleWrap.require('motion/mail/**/*.rb') do 6 | file('motion/mail/mail.rb').depends_on('motion/mail/result.rb') 7 | file('motion/mail/mail.rb').uses_framework('MessageUI') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /samples/media/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 4 | camera_controller = PlayController.alloc.init 5 | @window.rootViewController = camera_controller 6 | @window.makeKeyAndVisible 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /samples/camera/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 4 | camera_controller = CameraController.alloc.init 5 | @window.rootViewController = camera_controller 6 | @window.makeKeyAndVisible 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /samples/location/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def application(application, didFinishLaunchingWithOptions:launchOptions) 3 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 4 | places_list_controller = PlacesListController.alloc.init 5 | @window.rootViewController = places_list_controller 6 | @window.makeKeyAndVisible 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | before_install: 3 | - (ruby --version) 4 | - sudo chown -R travis ~/Library/RubyMotion 5 | - mkdir -p ~/Library/RubyMotion/build 6 | - sudo motion update 7 | script: 8 | - bundle install 9 | - bundle exec rake clean 10 | - bundle exec rake spec 11 | - bundle exec rake clean 12 | - bundle exec rake spec osx=true -------------------------------------------------------------------------------- /lib/bubble-wrap/test.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | BW.require 'motion/util/*.rb' 3 | BW.require 'motion/test_suite_delegate.rb' 4 | 5 | Motion::Project::App.setup do |app| 6 | app.development do 7 | if Motion::Project::App.osx? 8 | app.delegate_class = 'TestSuiteOSXDelegate' 9 | else 10 | app.delegate_class = 'TestSuiteDelegate' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /motion/media/media.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Media 3 | module_function 4 | 5 | def play_modal(*args, &block) 6 | Media::Player.new.retain.send(:play_modal, *args, &block) 7 | end 8 | 9 | def play(*args, &block) 10 | Media::Player.new.retain.send(:play, *args, &block) 11 | end 12 | end 13 | end 14 | 15 | ::Media = BubbleWrap::Media unless defined?(::Media) -------------------------------------------------------------------------------- /lib/bubble-wrap/ui.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("ui") do 4 | BubbleWrap.require('motion/util/constants.rb') 5 | BubbleWrap.require('motion/ui/**/*.rb') do 6 | file('motion/ui/pollute.rb').depends_on %w( 7 | motion/ui/ui_control_wrapper.rb 8 | motion/ui/ui_view_wrapper.rb 9 | motion/ui/ui_view_controller_wrapper.rb 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/motion/core/device_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Device do 2 | 3 | describe '.screen' do 4 | it 'return BubbleWrap::Screen' do 5 | BW::Device.screen.should == BW::Device::Screen 6 | end 7 | end 8 | 9 | describe '.retina?' do 10 | it 'delegates to BubbleWrap::Screen.retina?' do 11 | BW::Device.retina?.should == BW::Device::Screen.retina? 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /motion/core/device.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module_function 4 | 5 | # Shameless shorthand for accessing BubbleWrap::Screen 6 | def screen 7 | BubbleWrap::Device::Screen 8 | end 9 | 10 | # Delegates to BubbleWrap::Screen.retina? 11 | def retina? 12 | screen.retina? 13 | end 14 | end 15 | end 16 | ::Device = BubbleWrap::Device unless defined?(::Device) 17 | -------------------------------------------------------------------------------- /spec/motion/ui/pollute_spec.rb: -------------------------------------------------------------------------------- 1 | describe "UIKit pollution" do 2 | it "pollutes UIControl" do 3 | UIControl.ancestors.should.include BW::UIControlWrapper 4 | end 5 | 6 | it "pollutes UIView" do 7 | UIView.ancestors.should.include BW::UIViewWrapper 8 | end 9 | 10 | it "pollutes UIViewController" do 11 | UIViewController.ancestors.should.include BW::UIViewControllerWrapper 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/motion/core/osx/app_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::App do 2 | describe "OS X" do 3 | describe '.delegate' do 4 | it 'returns a TestSuiteDelegate' do 5 | App.delegate.should == NSApplication.sharedApplication.delegate 6 | end 7 | end 8 | 9 | describe '.shared' do 10 | it 'returns UIApplication.sharedApplication' do 11 | App.shared.should == NSApplication.sharedApplication 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /samples/gesture/app/controllers/drawing_view_controller.rb: -------------------------------------------------------------------------------- 1 | class DrawingViewController < UIViewController 2 | attr_reader :drawing_view 3 | 4 | def loadView 5 | screen = UIScreen.mainScreen.bounds 6 | self.view = UIView.alloc.initWithFrame screen 7 | self.view.backgroundColor = UIColor.whiteColor 8 | 9 | @drawing_view = RectView.alloc.initWithFrame [[screen.size.width/2-50,screen.size.height/2-50], [100,100]] 10 | self.view.addSubview @drawing_view 11 | end 12 | end -------------------------------------------------------------------------------- /lib/bubble-wrap/location.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("location") do 4 | BubbleWrap.require('motion/util/constants.rb') 5 | BubbleWrap.require('motion/core/string.rb') 6 | BubbleWrap.require('motion/location/**/*.rb') do 7 | file('motion/location/pollute.rb').depends_on 'motion/location/location.rb' 8 | file('motion/location/location.rb').depends_on 'motion/util/constants.rb' 9 | file('motion/location/location.rb').uses_framework('CoreLocation') 10 | end 11 | end -------------------------------------------------------------------------------- /samples/gesture/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | attr_reader :window 3 | 4 | def application(application, didFinishLaunchingWithOptions:launchOptions) 5 | setup_window 6 | true 7 | end 8 | 9 | # setup UI 10 | def setup_window 11 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 12 | @window.rootViewController = DrawingViewController.alloc.init 13 | @window.rootViewController.wantsFullScreenLayout = true 14 | @window.makeKeyAndVisible 15 | end 16 | end -------------------------------------------------------------------------------- /motion/sms/result.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module SMS 3 | class Result 4 | attr_accessor :result 5 | 6 | def initialize(result) 7 | self.result = result 8 | end 9 | 10 | def sent? 11 | self.result == MessageComposeResultSent 12 | end 13 | 14 | def canceled? 15 | self.result == MessageComposeResultCancelled 16 | end 17 | 18 | def failed? 19 | self.result == MessageComposeResultFailed 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /motion/ui/ui_view_controller_wrapper.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module UIViewControllerWrapper 3 | # Short hand to get the content frame 4 | # 5 | # Return content frame: the application frame - navigation bar frame 6 | def content_frame 7 | app_frame = App.frame 8 | navbar_height = self.navigationController.nil? ? 9 | 0 : self.navigationController.navigationBar.frame.size.height 10 | CGRectMake(0, 0, app_frame.size.width, app_frame.size.height - navbar_height) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /samples/gesture/app/views/drawing/rect_view.rb: -------------------------------------------------------------------------------- 1 | class RectView < GestureView 2 | def drawRect(rect) 3 | super(rect) 4 | 5 | context = UIGraphicsGetCurrentContext() 6 | CGContextSetAllowsAntialiasing(context, true); 7 | CGContextSetShouldAntialias(context, true); 8 | CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor) 9 | CGContextSetLineWidth(context, 10.0) 10 | CGContextAddRect(context, [[0,0], [self.frame.size.width,self.frame.size.height]]) 11 | CGContextFillPath(context) 12 | end 13 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | bubble-wrap (1.6.0) 5 | bubble-wrap-http (= 1.6.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | bacon (1.1.0) 11 | bubble-wrap-http (1.6.0) 12 | metaclass (0.0.1) 13 | mocha (0.11.4) 14 | metaclass (~> 0.0.1) 15 | mocha-on-bacon (0.2.1) 16 | mocha (>= 0.9.8) 17 | rake (0.9.2.2) 18 | 19 | PLATFORMS 20 | ruby 21 | 22 | DEPENDENCIES 23 | bacon 24 | bubble-wrap! 25 | mocha (= 0.11.4) 26 | mocha-on-bacon 27 | rake 28 | -------------------------------------------------------------------------------- /motion/core/ns_url_request.rb: -------------------------------------------------------------------------------- 1 | class NSURLRequest 2 | 3 | # Provides a to_s method so we can use inspect in instances and get 4 | # valuable information. 5 | def to_s 6 | "#<#{self.class}:#{self.object_id} - url: #{self.URL.description}, 7 | headers: #{self.allHTTPHeaderFields.inspect}, 8 | cache policy: #{self.cachePolicy}, Pipelining: #{self.HTTPShouldUsePipelining}, main doc url: #{mainDocumentURL},\ 9 | timeout: #{self.timeoutInterval}, network service type: #{self.networkServiceType} >" 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /motion/ui/ui_control_wrapper.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module UIControlWrapper 3 | def when(events, options = {}, &block) 4 | @callback ||= {} 5 | @callback[events] ||= [] 6 | 7 | unless options[:append] 8 | @callback[events] = [] 9 | removeTarget(nil, action: nil, forControlEvents: events) 10 | end 11 | 12 | @callback[events] << block 13 | block.weak! if BubbleWrap.use_weak_callbacks? 14 | addTarget(@callback[events].last, action:'call', forControlEvents: events) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /motion/core/ns_user_defaults.rb: -------------------------------------------------------------------------------- 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.to_s) 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.to_s) 13 | self.synchronize 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /motion/core/device/osx/screen.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module Screen 4 | 5 | module_function 6 | 7 | # Certifies that the device running the app has a Retina display 8 | # @return [TrueClass, FalseClass] true will be returned if the device has a Retina display, false otherwise. 9 | def retina?(screen=NSScreen.mainScreen) 10 | if screen.respondsToSelector('backingScaleFactor') && screen.backingScaleFactor == 2.0 11 | true 12 | else 13 | false 14 | end 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /motion/reactor/future.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | module Future 4 | 5 | # A future is a sugaring of a typical deferrable usage. 6 | def future arg, cb=nil, eb=nil, &blk 7 | arg = arg.call if arg.respond_to?(:call) 8 | 9 | if arg.respond_to?(:set_deferred_status) 10 | if cb || eb 11 | arg.callback(&cb) if cb 12 | arg.errback(&eb) if eb 13 | else 14 | arg.callback(&blk) if blk 15 | end 16 | end 17 | 18 | arg 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/bubble-wrap/http.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/version' unless defined?(BubbleWrap::VERSION) 2 | require 'bubble-wrap/loader' 3 | require 'bubble-wrap/network-indicator' 4 | require 'bubble-wrap-http' 5 | Motion::Project::App.warn "BubbleWrap::HTTP is deprecated and will be removed, see https://github.com/rubymotion/BubbleWrap/issues/308" 6 | Motion::Project::App.warn "Switch to a different networking library soon - consider AFNetworking: http://afnetworking.com/" 7 | Motion::Project::App.warn "You can use the 'bubble-wrap-http' gem if you need compatibility: https://github.com/rubymotion/BubbleWrap-HTTP" -------------------------------------------------------------------------------- /lib/bubble-wrap/reactor.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BW.require 'motion/reactor.rb' 4 | BW.require 'motion/reactor/**/*.rb' do 5 | file('motion/reactor.rb').depends_on Dir.glob('motion/reactor/**/*.rb') 6 | file('motion/reactor/timer.rb').depends_on 'motion/reactor/eventable.rb' 7 | file('motion/reactor/periodic_timer.rb').depends_on 'motion/reactor/eventable.rb' 8 | file('motion/reactor/deferrable.rb').depends_on ['motion/reactor/timer.rb', 'motion/reactor/future.rb'] 9 | file('motion/reactor/default_deferrable.rb').depends_on 'motion/reactor/deferrable.rb' 10 | end 11 | -------------------------------------------------------------------------------- /samples/osx/resources/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /lib/bubble-wrap/camera.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("camera") do 4 | BubbleWrap.require('motion/util/constants.rb') 5 | BubbleWrap.require('motion/core/device.rb') 6 | BubbleWrap.require('motion/core/device/*.rb') 7 | BubbleWrap.require('motion/core/device/ios/*.rb') do 8 | file('motion/core/device/ios/camera_wrapper.rb').depends_on 'motion/core/device/ios/camera.rb' 9 | file('motion/core/device/ios/camera.rb').depends_on ['motion/core/ios/device.rb', 'motion/util/constants.rb'] 10 | file('motion/core/device/ios/screen.rb').depends_on 'motion/core/ios/device.rb' 11 | end 12 | end -------------------------------------------------------------------------------- /lib/bubble-wrap/media.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/loader' 2 | 3 | BubbleWrap.require_ios("media") do 4 | BubbleWrap.require('motion/core/app.rb') 5 | BubbleWrap.require('motion/core/ios/app.rb') 6 | BubbleWrap.require('motion/core/ios/device.rb') 7 | BubbleWrap.require('motion/core/string.rb') 8 | BubbleWrap.require('motion/core/ns_notification_center.rb') 9 | BubbleWrap.require('motion/media/**/*.rb') do 10 | file('motion/media/media.rb').depends_on('motion/media/player.rb') 11 | file('motion/media/player.rb').depends_on 'motion/core/string.rb' 12 | file('motion/media/player.rb').uses_framework('MediaPlayer') 13 | end 14 | end -------------------------------------------------------------------------------- /spec/motion/core/ns_index_path_spec.rb: -------------------------------------------------------------------------------- 1 | describe "NSIndexPathWrap" do 2 | 3 | before do 4 | if App.osx? 5 | @index = NSIndexPath.indexPathWithIndex(3) 6 | else 7 | @index = NSIndexPath.indexPathForRow(0, inSection:3) 8 | end 9 | end 10 | 11 | it "should be able to use an array like accessor" do 12 | @index[0].should == @index.indexAtPosition(0) 13 | end 14 | 15 | it "should support the each iterator" do 16 | i = [] 17 | @index.each do |idx| 18 | i << idx 19 | end 20 | if App.osx? 21 | i.should == [3] 22 | else 23 | i.should == [3, 0] 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /motion/mail/result.rb: -------------------------------------------------------------------------------- 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 30 | -------------------------------------------------------------------------------- /motion/core/ns_index_path.rb: -------------------------------------------------------------------------------- 1 | module NSIndexPathWrap 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 24 | -------------------------------------------------------------------------------- /motion/core/ns_notification_center.rb: -------------------------------------------------------------------------------- 1 | class NSNotificationCenter 2 | def observers 3 | @observers ||= [] 4 | end 5 | 6 | def observe(name, object=nil, &proc) 7 | proc.weak! if proc && BubbleWrap.use_weak_callbacks? 8 | observer = self.addObserverForName(name, object:object, queue:NSOperationQueue.mainQueue, usingBlock:proc) 9 | observers << observer 10 | observer 11 | end 12 | 13 | def unobserve(observer) 14 | return unless observers.include?(observer) 15 | removeObserver(observer) 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 -------------------------------------------------------------------------------- /samples/location/app/models/places.rb: -------------------------------------------------------------------------------- 1 | class Places 2 | API_KEY = "" 3 | #See google places documentation at https://developers.google.com/places/documentation/ to obtain a key 4 | 5 | def self.load(places_list_controller) 6 | BW::Location.get do |result| 7 | BW::Location.stop 8 | BubbleWrap::HTTP.get("https://maps.googleapis.com/maps/api/place/search/json?location=#{result[:to].latitude},#{result[:to].longitude}&radius=500&sensor=false&key=#{API_KEY}") do |response| 9 | names = BW::JSON.parse(response.body.to_str)["results"].map{|r| r["name"]} 10 | places_list_controller.places_list = names 11 | places_list_controller.reloadData 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/bubble-wrap/core.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/version' unless defined?(BubbleWrap::VERSION) 2 | require 'bubble-wrap/loader' 3 | 4 | BubbleWrap.require('motion/core.rb') 5 | BubbleWrap.require('motion/core/*.rb') do 6 | file('motion/core/pollute.rb').depends_on 'motion/core/ns_index_path.rb' 7 | end 8 | BubbleWrap.require('motion/core/device/*.rb') 9 | 10 | BubbleWrap.require_ios do 11 | BubbleWrap.require('motion/core/ios/**/*.rb') 12 | BubbleWrap.require('motion/core/device/ios/**/*.rb') 13 | 14 | require 'bubble-wrap/camera' 15 | require 'bubble-wrap/ui' 16 | end 17 | 18 | BubbleWrap.require_osx do 19 | BubbleWrap.require('motion/core/osx/**/*.rb') 20 | BubbleWrap.require('motion/core/device/osx/**/*.rb') 21 | end 22 | -------------------------------------------------------------------------------- /motion/shortcut.rb: -------------------------------------------------------------------------------- 1 | # Make sure that 2 | # Both BubbleWrap and BW are defined. This file is depended on by everything 3 | # else in BubbleWrap, so don't stuff anything in here unless you know why 4 | # you're doing it. 5 | module BubbleWrap 6 | SETTINGS = {} 7 | 8 | def self.debug=(val) 9 | BubbleWrap::SETTINGS[:debug] = val 10 | end 11 | 12 | def self.debug? 13 | BubbleWrap::SETTINGS[:debug] 14 | end 15 | 16 | def self.use_weak_callbacks=(val) 17 | BubbleWrap::SETTINGS[:use_weak_callbacks] = val 18 | end 19 | 20 | def self.use_weak_callbacks? 21 | BubbleWrap::SETTINGS[:use_weak_callbacks] 22 | end 23 | 24 | def version 25 | BubbleWrap::VERSION 26 | end 27 | end 28 | 29 | BW = BubbleWrap unless defined?(BW) 30 | -------------------------------------------------------------------------------- /motion/reactor/timer.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | # Creates a one-time timer. 4 | class Timer 5 | include Eventable 6 | 7 | # Create a new timer that fires after a given number of seconds 8 | def initialize(leeway, callback=nil, &blk) 9 | queue = Dispatch::Queue.current 10 | @timer = Dispatch::Source.timer(leeway, Dispatch::TIME_FOREVER, 0.0, queue) do |src| 11 | begin 12 | (callback || blk).call 13 | trigger(:fired) 14 | ensure 15 | src.cancel! 16 | end 17 | end 18 | end 19 | 20 | # Cancel the timer 21 | def cancel 22 | @timer.cancel! if @timer 23 | trigger(:cancelled) 24 | true 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/motion/core/device/osx/screen_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Device::Screen do 2 | describe "OS X" do 3 | describe 'on retina enabled screen' do 4 | before do 5 | @screen = Object.new.tap do |o| 6 | def o.respondsToSelector(selector) 7 | return true if selector == 'backingScaleFactor' 8 | NSScreen.mainScreen.respondsToSelector(selector) 9 | end 10 | def o.backingScaleFactor 11 | 2.0 12 | end 13 | def o.method_missing(*args) 14 | NSScreen.mainScreen.send(*args) 15 | end 16 | end 17 | end 18 | 19 | describe '.retina?' do 20 | it 'returns true' do 21 | BW::Device::Screen.retina?(@screen).should == true 22 | end 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /motion/core/time.rb: -------------------------------------------------------------------------------- 1 | class Time 2 | 3 | def self.iso8601(time) 4 | cached_date_formatter("yyyy-MM-dd'T'HH:mm:ss'Z'"). 5 | dateFromString(time) 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 12 | 13 | def self.iso8601_with_fractional_seconds(time) 14 | cached_date_formatter("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"). 15 | dateFromString(time) 16 | end 17 | 18 | private 19 | 20 | def self.cached_date_formatter(dateFormat) 21 | Thread.current[:date_formatters] ||= {} 22 | Thread.current[:date_formatters][dateFormat] ||= 23 | NSDateFormatter.alloc.init.tap do |formatter| 24 | formatter.dateFormat = dateFormat 25 | formatter.timeZone = NSTimeZone.timeZoneWithAbbreviation "UTC" 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /motion/core.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module_function 3 | 4 | # @return [UIcolor] 5 | def rgb_color(r,g,b) 6 | rgba_color(r,g,b,1) 7 | end 8 | 9 | # @return [UIcolor] 10 | def rgba_color(r,g,b,a) 11 | r,g,b = [r,g,b].map { |i| i / 255.0} 12 | if a > 1.0 13 | a = a / 255.0 14 | end 15 | if App.osx? 16 | NSColor.colorWithDeviceRed(r, green: g, blue: b, alpha: a) 17 | else 18 | UIColor.colorWithRed(r, green: g, blue:b, alpha:a) 19 | end 20 | end 21 | 22 | def localized_string(key, value=nil) 23 | NSBundle.mainBundle.localizedStringForKey(key, value:value, table:nil) 24 | end 25 | 26 | # I had issues with #p on the device, this is a temporary workaround 27 | def p(arg) 28 | NSLog arg.inspect 29 | end 30 | 31 | def create_uuid 32 | uuid = CFUUIDCreate(nil) 33 | CFUUIDCreateString(nil, uuid) 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /samples/location/app/controllers/image_list_controller.rb: -------------------------------------------------------------------------------- 1 | class PlacesListController < UITableViewController 2 | attr_accessor :places_list 3 | 4 | def viewDidLoad 5 | @places_list = [] 6 | Places.load(self) 7 | view.dataSource = view.delegate = self 8 | end 9 | 10 | def viewWillAppear(animated) 11 | view.reloadData 12 | end 13 | 14 | def tableView(tableView, numberOfRowsInSection:section) 15 | @places_list.size 16 | end 17 | 18 | def reloadData 19 | view.reloadData 20 | end 21 | 22 | CellID = 'CellIdentifier' 23 | def tableView(tableView, cellForRowAtIndexPath:indexPath) 24 | cell = tableView.dequeueReusableCellWithIdentifier(CellID) || UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:CellID) 25 | placeItem= @places_list[indexPath.row] 26 | cell.textLabel.text = placeItem 27 | cell 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/bubble-wrap_spec.rb: -------------------------------------------------------------------------------- 1 | require 'mocha-on-bacon' 2 | require File.expand_path('../motion_stub.rb', __FILE__) 3 | require 'bubble-wrap' 4 | 5 | describe BubbleWrap do 6 | describe '.root' do 7 | it 'returns an absolute path' do 8 | BubbleWrap.root[0].should == '/' 9 | end 10 | end 11 | 12 | describe '.require' do 13 | it 'delegates to Requirement.scan' do 14 | BW::Requirement.expects(:scan) 15 | BW.require('foo') 16 | end 17 | 18 | it 'finds files with relative paths' do 19 | BW::Requirement.clear! 20 | BW.require '../motion/core.rb' 21 | BW::Requirement.files.member?(File.expand_path('../../../motion/core.rb', __FILE__)).should == true 22 | end 23 | 24 | it 'finds files with absolute paths' do 25 | BW::Requirement.clear! 26 | BW.require File.expand_path('../../../motion/core.rb', __FILE__) 27 | BW::Requirement.files.member?(File.expand_path('../../../motion/core.rb', __FILE__)).should == true 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /motion/reactor/eventable.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | # A simple mixin that adds events to your object. 4 | module Eventable 5 | 6 | # When `event` is triggered the block will execute 7 | # and be passed the arguments that are passed to 8 | # `trigger`. 9 | def on(event, method = nil, &blk) 10 | method_or_block = method ? method : blk 11 | __events__[event].push method_or_block 12 | end 13 | 14 | # When `event` is triggered, do not call the given 15 | # block any more 16 | def off(event, method = nil, &blk) 17 | method_or_block = method ? method : blk 18 | __events__[event].delete_if { |b| b == method_or_block } 19 | blk 20 | end 21 | 22 | # Trigger an event 23 | def trigger(event, *args) 24 | blks = __events__[event].clone 25 | blks.map do |blk| 26 | blk.call(*args) 27 | end 28 | end 29 | 30 | private 31 | 32 | def __events__ 33 | @__events__ ||= Hash.new { |h,k| h[k] = [] } 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/bubble-wrap/ext/motion_project_app.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Ext 3 | module BuildTask 4 | 5 | def self.extended(base) 6 | base.instance_eval do 7 | def setup_with_bubblewrap(*args, &block) 8 | bw_config = proc do |app| 9 | app.files = ::BubbleWrap::Requirement.files(app.files) 10 | app.files_dependencies ::BubbleWrap::Requirement.files_dependencies 11 | app.frameworks = ::BubbleWrap::Requirement.frameworks(app.frameworks) 12 | block.call(app) unless block.nil? 13 | end 14 | 15 | setup_without_bubblewrap *args, &bw_config 16 | end 17 | alias :setup_without_bubblewrap :setup 18 | alias :setup :setup_with_bubblewrap 19 | end 20 | end 21 | 22 | end 23 | 24 | module Platforms 25 | def osx? 26 | self.respond_to?(:template) && self.template == :osx 27 | end 28 | end 29 | end 30 | end 31 | 32 | Motion::Project::App.extend(BubbleWrap::Ext::BuildTask) 33 | 34 | Motion::Project::App.extend(BubbleWrap::Ext::Platforms) 35 | -------------------------------------------------------------------------------- /spec/motion/sms/result_spec.rb: -------------------------------------------------------------------------------- 1 | describe BW::SMS::Result do 2 | 3 | before do 4 | @subject = BW::SMS::Result.new(MessageComposeResultCancelled) 5 | end 6 | 7 | it "should set sent? when sent" do 8 | @subject.result = MessageComposeResultSent 9 | @subject.should.be.sent 10 | end 11 | 12 | it "should not set sent? when not sent" do 13 | @subject.result = MessageComposeResultFailed 14 | @subject.should.not.be.sent 15 | end 16 | 17 | it "should set canceled? when canceled" do 18 | @subject.result = MessageComposeResultCancelled 19 | @subject.should.be.canceled 20 | end 21 | 22 | it "should not set canceled? when not canceled" do 23 | @subject.result = MessageComposeResultFailed 24 | @subject.should.not.be.canceled 25 | end 26 | 27 | 28 | it "should set failed? when failed" do 29 | @subject.result = MessageComposeResultFailed 30 | @subject.should.be.failed 31 | end 32 | 33 | it "should not set failed? when not failed" do 34 | @subject.result = MessageComposeResultSent 35 | @subject.should.not.be.failed 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /spec/motion/core/ns_notification_center_spec.rb: -------------------------------------------------------------------------------- 1 | describe "NSNotificationCenter" do 2 | SampleNotification = "SampleNotification" 3 | 4 | after do 5 | @observer = nil 6 | end 7 | 8 | after do 9 | BW::App.notification_center.unobserve(@observer) if @observer 10 | end 11 | 12 | it "return notification center" do 13 | BW::App.notification_center.should.not.be.nil 14 | end 15 | 16 | it "add observer" do 17 | @notified = false 18 | @observer = BW::App.notification_center.observe(SampleNotification) do |note| 19 | @notified = true 20 | note.should.is_a NSNotification 21 | note.object.class.should == Time 22 | note.userInfo.should.not.be.nil 23 | note.userInfo[:status].should == "ok" 24 | end 25 | 26 | lambda { 27 | BW::App.notification_center.post SampleNotification, Time.now, {:status => "ok"} 28 | }.should.change { @notified } 29 | end 30 | 31 | it "remove observer" do 32 | lambda { 33 | @observer = BW::App.notification_center.observe(SampleNotification) {} 34 | BW::App.notification_center.unobserve(@observer) 35 | }.should.not.change { BW::App.notification_center.observers.size } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/motion/ui/ui_view_controller_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe BW::UIViewControllerWrapper do 2 | describe "#content_frame" do 3 | describe "given a UIViewController" do 4 | before do 5 | @subject = UIViewController.alloc.init 6 | end 7 | 8 | it "has the correct content frame" do 9 | height = App.frame.size.height 10 | expected = CGRectMake(0, 0, App.frame.size.width, height) 11 | 12 | @subject.content_frame.should.equal(expected) 13 | end 14 | end 15 | 16 | ############################################################################################### 17 | 18 | describe "given a UIViewController inside a UINavigationController" do 19 | before do 20 | @subject = UIViewController.alloc.init 21 | 22 | UINavigationController.alloc.initWithRootViewController(@subject) 23 | end 24 | 25 | it "has the correct content frame" do 26 | height = App.frame.size.height 27 | height -= @subject.navigationController.navigationBar.frame.size.height 28 | expected = CGRectMake(0, 0, App.frame.size.width, height) 29 | 30 | @subject.content_frame.should.equal(expected) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /motion/reactor/periodic_timer.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | # Creates a repeating timer. 4 | class PeriodicTimer 5 | include Eventable 6 | 7 | attr_accessor :interval 8 | 9 | # Create a new timer that fires after a given number of seconds 10 | def initialize(interval, *args, &blk) 11 | callback = args.first.respond_to?(:call) ? args.first : blk 12 | raise ArgumentError, "No callback or block supplied to periodic timer" unless callback 13 | callback.weak! if callback && BubbleWrap.use_weak_callbacks? 14 | 15 | options = args.last.is_a?(Hash) ? args.last : {} 16 | if options[:common_modes] 17 | NSLog "[DEPRECATED - Option :common_modes] a Run Loop Mode is no longer needed." 18 | end 19 | 20 | self.interval = interval 21 | 22 | leeway = interval 23 | queue = Dispatch::Queue.current 24 | @timer = Dispatch::Source.timer(leeway, interval, 0.0, queue) do 25 | callback.call 26 | trigger(:fired) 27 | end 28 | end 29 | 30 | # Cancel the timer 31 | def cancel 32 | @timer.cancel! 33 | trigger(:cancelled) 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | $:.unshift("/Library/RubyMotion/lib") 3 | if ENV['osx'] 4 | require 'motion/project/template/osx' 5 | else 6 | require 'motion/project/template/ios' 7 | end 8 | Bundler.setup 9 | Bundler.require 10 | 11 | require 'bubble-wrap/all' 12 | require 'bubble-wrap/test' 13 | 14 | module Motion 15 | module Project 16 | class Config 17 | def spec_files=(spec_files) 18 | @spec_files = spec_files 19 | end 20 | end 21 | end 22 | end 23 | 24 | Motion::Project::App.setup do |app| 25 | app.name = 'testSuite' 26 | app.identifier = 'io.bubblewrap.testSuite' 27 | app.specs_dir = './spec/motion' 28 | app.spec_files 29 | if Motion::Project::App.osx? 30 | app.spec_files -= Dir.glob("./spec/motion/**/ios/**.rb") 31 | ["font", "location", "media", "ui", "mail", "sms", "network-indicator"].each do |package| 32 | app.spec_files -= Dir.glob("./spec/motion/#{package}/**/*.rb") 33 | end 34 | else 35 | app.spec_files -= Dir.glob("./spec/motion/**/osx/**.rb") 36 | end 37 | app.version = '1.2.3' 38 | end 39 | 40 | namespace :spec do 41 | task :lib do 42 | sh "bacon #{Dir.glob("spec/lib/**/*_spec.rb").join(' ')}" 43 | end 44 | 45 | task :motion => 'spec' 46 | 47 | task :all => [:lib, :motion] 48 | end 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENCE 2 | 3 | MIT: http://mattaimonetti.mit-license.org 4 | 5 | ------------------------------------------------ 6 | 7 | The MIT License (MIT) 8 | Copyright © 2012 Matt Aimonetti 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. 27 | -------------------------------------------------------------------------------- /lib/bubble-wrap/requirement/path_manipulation.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | class Requirement 3 | module PathManipulation 4 | 5 | def convert_caller_to_root_path(path) 6 | path = convert_caller_to_path path 7 | path = convert_to_absolute_path path 8 | strip_up_to_last_lib path 9 | end 10 | 11 | def convert_caller_to_path(string) 12 | chunks = string.split(':') 13 | if chunks.size >= 3 14 | string = chunks[0..-3].join(':') 15 | string = File.dirname(string) 16 | end 17 | string 18 | end 19 | 20 | def convert_to_absolute_path(path) 21 | File.expand_path(path) 22 | end 23 | 24 | def strip_up_to_last_lib(path) 25 | if path =~ /\/lib$/ 26 | path = path.gsub(/\/lib$/, "") 27 | else 28 | path = path.split('lib') 29 | path = if path.size > 1 30 | path[0..-2].join('lib') 31 | else 32 | path[0] 33 | end 34 | path = path[0..-2] if path[-1] == '/' 35 | end 36 | path 37 | end 38 | 39 | def convert_to_relative(path,root) 40 | path = path.gsub(root,'') 41 | path = path[1..-1] if path[0] == '/' 42 | path 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /motion/core/json.rb: -------------------------------------------------------------------------------- 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 | return nil unless str_data 17 | data = str_data.respond_to?(:to_data) ? str_data.to_data : str_data 18 | opts = NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments 19 | error = Pointer.new(:id) 20 | obj = NSJSONSerialization.JSONObjectWithData(data, options:opts, error:error) 21 | raise ParserError, error[0].description if error[0] 22 | if block_given? 23 | yield obj 24 | else 25 | obj 26 | end 27 | 28 | end 29 | 30 | def self.generate(obj) 31 | NSJSONSerialization.dataWithJSONObject(obj, options:0, error:nil).to_str 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/motion/util/constants_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Constants do 2 | describe ".get" do 3 | BubbleWrap::Constants.register NSStringEncodingConversionAllowLossy, NSStringEncodingConversionExternalRepresentation 4 | 5 | 6 | it "should return integer passed" do 7 | BW::Constants.get("anything", 5).should == 5 8 | end 9 | 10 | it "should return integer for decimal passed" do 11 | BW::Constants.get("anything", 5.0).should == 5 12 | end 13 | 14 | it "should return the correct integer for a string" do 15 | BW::Constants.get("NSStringEncodingConversion", "allow_lossy").should == NSStringEncodingConversionAllowLossy 16 | end 17 | 18 | it "should return the correct integer for a symbol" do 19 | BW::Constants.get("NSStringEncodingConversion", :allow_lossy).should == NSStringEncodingConversionAllowLossy 20 | end 21 | 22 | it "should bitmask array values" do 23 | BW::Constants.get("NSStringEncodingConversion", :allow_lossy, :external_representation).should == (NSStringEncodingConversionAllowLossy | NSStringEncodingConversionExternalRepresentation) 24 | end 25 | 26 | if App.ios? 27 | it "should return an array of string constant values" do 28 | BW::Constants.get("UIActivityType", [:air_drop, :print]).should == ["com.apple.UIKit.activity.AirDrop", "com.apple.UIKit.activity.Print"] 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bubble-wrap.gemspec: -------------------------------------------------------------------------------- 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', 'James Harton', 'Clay Allsopp', 'Dylan Markow', 'Jan Weinkauff', 'Marin Usalj'] 6 | gem.email = ['mattaimonetti@gmail.com', 'francis@ignition.hk', 'james@sociable.co.nz', 'clay.allsopp@gmail.com', 'dylan@dylanmarkow.com', 'jan@dreimannzelt.de', 'mneorr@gmail.com'] 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 = 'http://bubblewrap.io/' 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.test_files = gem.files.grep(%r{^(test|spec|lib_spec|features)/}) 13 | gem.name = 'bubble-wrap' 14 | gem.require_paths = ['lib'] 15 | gem.version = BubbleWrap::VERSION 16 | 17 | gem.extra_rdoc_files = gem.files.grep(%r{motion}) 18 | 19 | gem.add_dependency 'bubble-wrap-http', BubbleWrap::VERSION 20 | gem.add_development_dependency 'mocha', '0.11.4' 21 | gem.add_development_dependency 'mocha-on-bacon' 22 | gem.add_development_dependency 'bacon' 23 | gem.add_development_dependency 'rake' 24 | end 25 | -------------------------------------------------------------------------------- /samples/media/app/controllers/play_controller.rb: -------------------------------------------------------------------------------- 1 | class PlayController < UIViewController 2 | attr_accessor :buttons 3 | 4 | def init 5 | super.tap do 6 | @buttons = [] 7 | end 8 | end 9 | 10 | def viewDidLoad 11 | super 12 | 13 | self.view.addSubview(build_button("Modal", "tapped_modal")) 14 | self.view.addSubview(build_button("Frame", "tapped_frame")) 15 | self.view.backgroundColor = UIColor.whiteColor 16 | end 17 | 18 | def build_button(title, callback) 19 | button = UIButton.buttonWithType(UIButtonTypeRoundedRect) 20 | button.setTitle(title, forState:UIControlStateNormal) 21 | button.sizeToFit 22 | 23 | rect = self.buttons.empty? ? CGRectMake(0, 0, 0, 0) : self.buttons.last.frame 24 | 25 | button.frame = [[rect.origin.x, rect.origin.y + rect.size.height + 10], button.frame.size] 26 | button.addTarget(self, action: callback, forControlEvents:UIControlEventTouchUpInside) 27 | 28 | self.buttons << button 29 | button 30 | end 31 | 32 | def local_file 33 | NSURL.fileURLWithPath(File.join(NSBundle.mainBundle.resourcePath, 'test.mp3')) 34 | end 35 | 36 | def tapped_modal 37 | BW::Media.play_modal(local_file) 38 | end 39 | 40 | def tapped_frame 41 | BW::Media.play(local_file) do |media_player| 42 | media_player.view.frame = [[10, 140], [self.view.frame.size.width - 20, 100]] 43 | self.view.addSubview media_player.view 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/lib/bubble-wrap/ext/motion_project_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'mocha-on-bacon' 2 | require File.expand_path('../../../motion_stub', __FILE__) 3 | require 'bubble-wrap' 4 | 5 | describe BubbleWrap::Ext::ConfigTask do 6 | 7 | before do 8 | klass = Class.new do 9 | def initialize 10 | @files = [ '/fake/a', '/fake/b' ] 11 | @dependencies = {} 12 | end 13 | 14 | def files_dependencies 15 | end 16 | end 17 | klass.send(:include, BubbleWrap::Ext::ConfigTask) 18 | @subject = klass.new 19 | end 20 | 21 | describe '.included' do 22 | it 'aliases :files_dependencies to :files_dependencies_without_bubblewrap' do 23 | @subject.respond_to?(:files_dependencies_without_bubblewrap).should == true 24 | end 25 | 26 | it 'aliass :files_dependencies_with_bubblewrap to :files_dependencies' do 27 | @subject.method(:files_dependencies).should == @subject.method(:files_dependencies_with_bubblewrap) 28 | end 29 | end 30 | 31 | describe '#path_matching_expression' do 32 | it 'returns a regular expression' do 33 | @subject.path_matching_expression.is_a?(Regexp).should == true 34 | end 35 | end 36 | 37 | describe '#files_dependencies_with_bubblewrap' do 38 | it 'should call #path_matching_expression' do 39 | @subject.expects(:path_matching_expression).twice().returns(/^\.?\//) 40 | @subject.files_dependencies_with_bubblewrap '/fake/a' => '/fake/b' 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /motion/util/deprecated.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Deprecated 3 | 4 | class DeprecatedError < StandardError; end 5 | 6 | def deprecated(method_sym, version) 7 | unless method_sym.kind_of?(Symbol) 8 | raise ArgumentError, "deprecated() requires symbols for its first argument." 9 | end 10 | 11 | scope = nil 12 | alias_scope = nil 13 | if self.methods.include?(method_sym) 14 | scope = :define_singleton_method 15 | alias_scope = (class << self; self end) 16 | elsif self.instance_methods.include?(method_sym) 17 | scope = :define_method 18 | alias_scope = self 19 | else 20 | raise ArgumentError, "Method not found for deprecated() - #{method_sym}" 21 | end 22 | 23 | 24 | send(scope, "#{method_sym}_with_deprecation", ->(*args, &block) { 25 | fail = BubbleWrap.version.to_s >= version.to_s 26 | if fail 27 | raise DeprecatedError, "#{method_sym} was deprecated and removed in BubbleWrap #{version}" 28 | else 29 | NSLog "#{method_sym} is deprecated -- it will be removed in BubbleWrap #{version}" 30 | send("#{method_sym}_without_deprecation", *args, &block) 31 | end 32 | }) 33 | 34 | alias_scope.send(:alias_method, "#{method_sym}_without_deprecation", method_sym) 35 | alias_scope.send(:alias_method, method_sym, "#{method_sym}_with_deprecation") 36 | end 37 | 38 | def self.included(base) 39 | base.extend(self) 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /lib/bubble-wrap/loader.rb: -------------------------------------------------------------------------------- 1 | unless defined?(Motion::Project::Config) 2 | raise 'This file must be required within a RubyMotion project Rakefile.' 3 | end 4 | 5 | unless defined?(BubbleWrap::LOADER_PRESENT) 6 | require 'bubble-wrap/version' unless defined?(VERSION) 7 | if Gem::Version.new(Motion::Version) < Gem::Version.new(BubbleWrap::MIN_MOTION_VERSION) 8 | raise "BubbleWrap #{BubbleWrap::VERSION} requires at least RubyMotion #{BubbleWrap::MIN_MOTION_VERSION}" 9 | end 10 | 11 | require 'bubble-wrap/ext' 12 | require 'bubble-wrap/requirement' 13 | 14 | module BubbleWrap 15 | 16 | LOADER_PRESENT = true 17 | module_function 18 | 19 | def root 20 | File.expand_path('../../../', __FILE__) 21 | end 22 | 23 | def require(file_spec, &block) 24 | Requirement.scan(caller.first, file_spec, &block) 25 | end 26 | 27 | def require_ios(requirement = nil, &callback) 28 | if !Motion::Project::App.osx? 29 | callback.call 30 | else 31 | puts "bubble-wrap/#{requirement} requires iOS to use." if requirement 32 | end 33 | end 34 | 35 | def require_osx(requirement = nil, &callback) 36 | if Motion::Project::App.osx? 37 | callback.call 38 | else 39 | puts "bubble-wrap/#{requirement} requires OS X to use." if requirement 40 | end 41 | end 42 | end 43 | 44 | BW = BubbleWrap unless defined?(BW) 45 | 46 | BW.require 'motion/shortcut.rb' 47 | BW.require 'lib/bubble-wrap/version.rb' 48 | BW.require 'motion/util/deprecated.rb' 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/motion/mail/result_spec.rb: -------------------------------------------------------------------------------- 1 | describe BW::Mail::Result do 2 | 3 | before do 4 | @subject = BW::Mail::Result.new(MFMailComposeResultCancelled, nil) 5 | end 6 | 7 | it "should set sent? when sent" do 8 | @subject.result = MFMailComposeResultSent 9 | @subject.should.be.sent 10 | end 11 | 12 | it "should not set sent? when not sent" do 13 | @subject.result = MFMailComposeResultCancelled 14 | @subject.should.not.be.sent 15 | end 16 | 17 | it "should set canceled? when canceled" do 18 | @subject.result = MFMailComposeResultCancelled 19 | @subject.should.be.canceled 20 | end 21 | 22 | it "should not set canceled? when not canceled" do 23 | @subject.result = MFMailComposeResultSent 24 | @subject.should.not.be.canceled 25 | end 26 | 27 | it "should set saved? when saved" do 28 | @subject.result = MFMailComposeResultSaved 29 | @subject.should.be.saved 30 | end 31 | 32 | it "should not set saved? when not saved" do 33 | @subject.result = MFMailComposeResultFailed 34 | @subject.should.not.be.saved 35 | end 36 | 37 | it "should set failed? when failed" do 38 | @subject.result = MFMailComposeResultFailed 39 | @subject.should.be.failed 40 | end 41 | 42 | it "should not set failed? when not failed" do 43 | @subject.result = MFMailComposeResultSent 44 | @subject.should.not.be.failed 45 | end 46 | 47 | it "should set failed? when error" do 48 | @subject.result = MFMailComposeResultCancelled 49 | @subject.error = :errored 50 | @subject.should.be.failed 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /samples/camera/app/controllers/camera_controller.rb: -------------------------------------------------------------------------------- 1 | class CameraController < UIViewController 2 | attr_accessor :buttons 3 | 4 | def init 5 | super.tap do 6 | @buttons = [] 7 | end 8 | end 9 | 10 | def viewDidLoad 11 | super 12 | 13 | self.view.addSubview(build_button("Library", :any)) 14 | self.view.addSubview(build_button("Front", :front)) if BW::Device.camera.front? 15 | self.view.addSubview(build_button("Rear", :rear)) if BW::Device.camera.rear? 16 | end 17 | 18 | def build_button(title, camera_method) 19 | button = UIButton.buttonWithType(UIButtonTypeRoundedRect) 20 | button.setTitle(title, forState:UIControlStateNormal) 21 | button.sizeToFit 22 | 23 | rect = self.buttons.empty? ? CGRectMake(0, 0, 0, 0) : self.buttons.last.frame 24 | 25 | button.frame = [[rect.origin.x, rect.origin.y + rect.size.height + 10], button.frame.size] 26 | 27 | button.when UIControlEventTouchUpInside do 28 | BW::Device.camera.send(camera_method).picture(media_types: [:image]) do |result| 29 | image_view = build_image_view(result[:original_image]) 30 | self.view.addSubview(image_view) 31 | 32 | self.buttons.each { |button| self.view.bringSubviewToFront(button) } 33 | end 34 | end 35 | 36 | self.buttons << button 37 | button 38 | end 39 | 40 | def build_image_view(image) 41 | image_view = UIImageView.alloc.initWithImage(image) 42 | image_view.frame = [CGPointZero, self.view.frame.size] 43 | image_view.center = [self.view.frame.size.width / 2, self.view.frame.size.height / 2] 44 | image_view 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /motion/core/persistence.rb: -------------------------------------------------------------------------------- 1 | # Persistence module built on top of NSUserDefaults 2 | module BubbleWrap 3 | module Persistence 4 | module_function 5 | 6 | def app_key 7 | @app_key ||= BubbleWrap::App.identifier 8 | end 9 | 10 | def []=(key, value) 11 | storage.setObject(value, forKey: storage_key(key)) 12 | storage.synchronize 13 | end 14 | 15 | def [](key) 16 | value = storage.objectForKey storage_key(key) 17 | 18 | # RubyMotion currently has a bug where the strings returned from 19 | # standardUserDefaults are missing some methods (e.g. to_data). 20 | # And because the returned object is slightly different than a normal 21 | # String, we can't just use `value.is_a?(String)` 22 | value.class.to_s == 'String' ? value.dup : value 23 | end 24 | 25 | def merge(values) 26 | values.each do |key, value| 27 | storage.setObject(value, forKey: storage_key(key)) 28 | end 29 | storage.synchronize 30 | end 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 | 39 | def storage 40 | NSUserDefaults.standardUserDefaults 41 | end 42 | 43 | def storage_key(key) 44 | "#{app_key}_#{key}" 45 | end 46 | 47 | def all 48 | hash = storage.dictionaryRepresentation.select{|k,v| k.start_with?(app_key) } 49 | new_hash = {} 50 | hash.each do |k,v| 51 | new_hash[k.sub("#{app_key}_", '')] = v 52 | end 53 | new_hash 54 | end 55 | end 56 | 57 | end 58 | ::Persistence = BubbleWrap::Persistence unless defined?(::Persistence) 59 | -------------------------------------------------------------------------------- /motion/reactor/queue.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | # A GCD scheduled, linear queue. 4 | # 5 | # This class provides a simple “Queue” like abstraction on top of the 6 | # GCD scheduler. 7 | # 8 | # Useful as an API sugar for stateful protocols 9 | # 10 | # q = BubbleWrap::Reactor::Queue.new 11 | # q.push('one', 'two', 'three') 12 | # 3.times do 13 | # q.pop{ |msg| puts(msg) } 14 | # end 15 | class Queue 16 | 17 | # Create a new queue 18 | def initialize 19 | @items = [] 20 | end 21 | 22 | # Is the queue empty? 23 | def empty? 24 | @items.empty? 25 | end 26 | 27 | # The size of the queue 28 | def size 29 | @items.size 30 | end 31 | 32 | # Push items onto the work queue. The items will not appear in the queue 33 | # immediately, but will be scheduled for addition. 34 | def push(*items) 35 | ::BubbleWrap::Reactor.schedule do 36 | @items.push(*items) 37 | @popq.shift.call @items.shift until @items.empty? || @popq.empty? 38 | end 39 | end 40 | 41 | # Pop items off the queue, running the block on the work queue. The pop 42 | # will not happen immediately, but at some point in the future, either 43 | # in the next tick, if the queue has data, or when the queue is populated. 44 | def pop(*args, &blk) 45 | cb = proc do 46 | blk.call(*args) 47 | end 48 | ::BubbleWrap::Reactor.schedule do 49 | if @items.empty? 50 | @popq << cb 51 | else 52 | cb.call @items.shift 53 | end 54 | end 55 | nil # Always returns nil 56 | end 57 | 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /motion/util/constants.rb: -------------------------------------------------------------------------------- 1 | # Stupid hack because the RubyMotion dependency detection has a bug. 2 | module BubbleWrap 3 | module Constants 4 | module_function 5 | 6 | # Looks like RubyMotiononly adds UIKit constants 7 | # at compile time. If you don't use these 8 | # directly in your code, they don't get added 9 | # to Kernel and Constants.get crashes. 10 | # Examples 11 | # Constants.register UIReturnKeyDone, UIReturnKeyNext 12 | def register(*ui_constants) 13 | # do nothing, just get the constants in the code 14 | end 15 | 16 | # @param [String] base of the constant 17 | # @param [Integer, NSArray, String, Symbol] the suffix of the constant 18 | # when NSArray, will return the bitmask of all suffixes in the array 19 | # @return [Integer] the constant for this base and suffix 20 | # Examples 21 | # get("UIReturnKey", :done) => UIReturnKeyDone == 9 22 | # get("UIReturnKey", "done") => UIReturnKeyDone == 9 23 | # get("UIReturnKey", 9) => 9 24 | # get("UIImagePickerControllerSourceType", ["photo_library", "camera", "saved_photos_album"]) => 3 25 | # get("UIActivityType", [:air_drop, :print]) => ["com.apple.UIKit.activity.AirDrop", "com.apple.UIKit.activity.Print"] 26 | def get(base, *values) 27 | value = values.size == 1 ? values.first : values.flatten 28 | case value 29 | when Numeric 30 | value.to_i 31 | when NSArray 32 | unless get(base, value.first).is_a? Fixnum 33 | value.map { |v| get(base, v) } 34 | else 35 | value.reduce { |i, j| 36 | get(base, i) | get(base, j) 37 | } 38 | end 39 | else 40 | value = value.to_s.camelize 41 | Kernel.const_get("#{base}#{value}") 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/lib/bubble-wrap/requirement/path_manipulation_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../../../lib/bubble-wrap/requirement/path_manipulation', __FILE__) 2 | 3 | describe BubbleWrap::Requirement::PathManipulation do 4 | 5 | before do 6 | @subject = Object.new 7 | @subject.extend BubbleWrap::Requirement::PathManipulation 8 | end 9 | 10 | describe '#convert_caller_to_path' do 11 | it 'strips off from the second-to-last colon' do 12 | @subject.convert_caller_to_path("/fake/:path/foo:91:in `fake_method'"). 13 | should == '/fake/:path' 14 | end 15 | 16 | it 'leaves plain old paths unmolested' do 17 | @subject.convert_caller_to_path("/fake/path"). 18 | should == '/fake/path' 19 | end 20 | end 21 | 22 | describe '#convert_to_absolute_path' do 23 | it 'converts relative paths to absolute paths' do 24 | @subject.convert_to_absolute_path('foo')[0].should == '/' 25 | end 26 | 27 | it "doesn't modify absolute paths" do 28 | @subject.convert_to_absolute_path('/foo').should == '/foo' 29 | end 30 | end 31 | 32 | describe '#strip_up_to_last_lib' do 33 | it 'strips off from the last lib' do 34 | @subject.strip_up_to_last_lib('/fake/lib/dir/lib/foo'). 35 | should == '/fake/lib/dir' 36 | end 37 | 38 | it "strips off only a trailing lib" do 39 | @subject.strip_up_to_last_lib('/fake/lib/dir/lib'). 40 | should == '/fake/lib/dir' 41 | end 42 | 43 | it "doesn't modify the path otherwise" do 44 | @subject.strip_up_to_last_lib('/fake/path'). 45 | should == '/fake/path' 46 | end 47 | end 48 | 49 | describe "#convert_to_relative" do 50 | it 'strips off the root portion' do 51 | @subject.convert_to_relative('/foo/bar/baz', '/foo'). 52 | should == 'bar/baz' 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /motion/ui/ui_activity_view_controller_wrapper.rb: -------------------------------------------------------------------------------- 1 | module BW 2 | class UIActivityViewController < ::UIActivityViewController 3 | class << self 4 | def new(options = {}, presenting_controller = nil, &block) 5 | options = { 6 | activities: nil, 7 | animated: true 8 | }.merge(options) 9 | 10 | if options[:item] || options[:items] 11 | items = Array(options[:item] || options[:items]) 12 | else 13 | raise ArgumentError, "You must specify at least one item - #{options.inspect}" 14 | end 15 | 16 | vc = alloc.initWithActivityItems(items, applicationActivities:options[:activities]) 17 | vc.excludedActivityTypes = BW::Constants.get("UIActivityType", Array(options[:excluded])) if options[:excluded] 18 | 19 | unless block.nil? 20 | block.weak! if BubbleWrap.use_weak_callbacks? 21 | vc.setCompletionHandler block 22 | end 23 | 24 | presenting_controller ||= App.window.rootViewController.presentedViewController # May be nil, but handles use case of container views 25 | presenting_controller ||= App.window.rootViewController 26 | 27 | presenting_controller.presentViewController(vc, animated:options[:animated], completion: lambda {}) 28 | vc 29 | end 30 | 31 | end 32 | end 33 | 34 | # UIActivityTypes 35 | Constants.register( 36 | UIActivityTypePostToFacebook, 37 | UIActivityTypePostToTwitter, 38 | UIActivityTypePostToWeibo, 39 | UIActivityTypeMessage, 40 | UIActivityTypeMail, 41 | UIActivityTypePrint, 42 | UIActivityTypeCopyToPasteboard, 43 | UIActivityTypeAssignToContact, 44 | UIActivityTypeSaveToCameraRoll, 45 | UIActivityTypeAddToReadingList, 46 | UIActivityTypePostToFlickr, 47 | UIActivityTypePostToVimeo, 48 | UIActivityTypePostToTencentWeibo, 49 | UIActivityTypeAirDrop 50 | ) 51 | end 52 | -------------------------------------------------------------------------------- /motion/network-indicator/network-indicator.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module NetworkIndicator 3 | DELAY = 0.2 4 | 5 | module_function 6 | 7 | def counter 8 | @counter ||= 0 9 | end 10 | 11 | def show 12 | if Dispatch::Queue.current.to_s == 'com.apple.main-thread' 13 | @counter = self.counter + 1 14 | self.update_spinner 15 | else 16 | Dispatch::Queue.main.async do 17 | self.show 18 | end 19 | end 20 | end 21 | 22 | def hide 23 | if Dispatch::Queue.current.to_s == 'com.apple.main-thread' 24 | @counter = [self.counter - 1, 0].max 25 | if self.counter == 0 26 | if @hide_indicator_timer 27 | @hide_indicator_timer.invalidate 28 | end 29 | @hide_indicator_timer = NSTimer.timerWithTimeInterval(DELAY - 0.01, target: self, selector: :update_spinner_timer, userInfo: nil, repeats: false) 30 | NSRunLoop.mainRunLoop.addTimer(@hide_indicator_timer, forMode:NSRunLoopCommonModes) 31 | end 32 | else 33 | Dispatch::Queue.main.async do 34 | self.hide 35 | end 36 | end 37 | end 38 | 39 | def update_spinner_timer 40 | update_spinner 41 | end 42 | 43 | def update_spinner 44 | if Dispatch::Queue.current.to_s == 'com.apple.main-thread' 45 | if @hide_indicator_timer 46 | @hide_indicator_timer.invalidate 47 | @hide_indicator_timer = nil 48 | end 49 | UIApplication.sharedApplication.networkActivityIndicatorVisible = (@counter > 0) 50 | else 51 | Dispatch::Queue.main.async do 52 | self.update_spinner 53 | end 54 | end 55 | end 56 | 57 | def visible? 58 | UIApplication.sharedApplication.networkActivityIndicatorVisible? 59 | end 60 | 61 | def reset! 62 | @counter = 0 63 | self.update_spinner 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /motion/sms/sms.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module SMS 3 | 4 | module_function 5 | 6 | # Base method to create your in-app mail 7 | # --------------------------------------- 8 | # EX 9 | # BW::SMS.compose ( 10 | # { 11 | # delegate: self, # optional, will use root view controller by default 12 | # to: [ "1(234)567-8910" ], 13 | # message: "This is my message. It isn't very long.", 14 | # animated: false 15 | # }) {|result, error| 16 | # result.sent? # => boolean 17 | # result.canceled? # => boolean 18 | # result.failed? # => boolean 19 | # error # => NSError 20 | # } 21 | 22 | def compose(options = {}, &callback) 23 | @delegate = options[:delegate] || App.window.rootViewController 24 | @callback = callback 25 | @callback.weak! if @callback && BubbleWrap.use_weak_callbacks? 26 | 27 | @message_controller = create_message_controller(options) 28 | @message_is_animated = options[:animated] == false ? false : true 29 | @delegate.presentModalViewController(@message_controller, animated: @message_is_animated) 30 | end 31 | 32 | def create_message_controller(options = {}) 33 | message_controller = MFMessageComposeViewController.alloc.init 34 | message_controller.messageComposeDelegate = self 35 | message_controller.body = options[:message] 36 | message_controller.recipients = Array(options[:to]) 37 | message_controller 38 | end 39 | 40 | # Event when the MFMessageComposeViewController is closed 41 | # ------------------------------------------------------------- 42 | # the callback is fired if it was present in the constructor 43 | 44 | def messageComposeViewController(controller, didFinishWithResult: result) 45 | @delegate.dismissModalViewControllerAnimated(@message_is_animated) 46 | @callback.call Result.new(result) if @callback 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/motion/core/device/ios/device_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Device do 2 | describe "iOS" do 3 | describe 'on iPhone' do 4 | before do 5 | @idiom = UIUserInterfaceIdiomPhone 6 | end 7 | 8 | describe '.iphone?' do 9 | it 'returns true' do 10 | BW::Device.iphone?(@idiom).should == true 11 | end 12 | end 13 | 14 | describe '.ipad?' do 15 | it 'returns false' do 16 | BW::Device.ipad?(@idiom).should == false 17 | end 18 | end 19 | 20 | describe '.long_screen?' do 21 | it 'returns true if screen is wide' do 22 | BW::Device.long_screen?(@idiom, 568.0).should == true 23 | end 24 | 25 | it 'returns false if screen is not wide' do 26 | BW::Device.long_screen?(@idiom, 480.0).should == false 27 | end 28 | end 29 | end 30 | 31 | describe 'on iPad' do 32 | before do 33 | @idiom = UIUserInterfaceIdiomPad 34 | end 35 | 36 | describe '.iphone?' do 37 | it 'returns false' do 38 | BW::Device.iphone?(@idiom).should == false 39 | end 40 | end 41 | 42 | describe '.ipad?' do 43 | it 'returns true' do 44 | BW::Device.ipad?(@idiom).should == true 45 | end 46 | end 47 | 48 | describe '.long_screen?' do 49 | it 'always not a widescreen' do 50 | BW::Device.long_screen?(@idiom, 1024.0).should == false 51 | end 52 | end 53 | end 54 | 55 | describe '.simulator?' do 56 | it 'returns true' do 57 | BW::Device.simulator?.should == true 58 | end 59 | end 60 | 61 | describe '.ios_version' do 62 | it 'returns true' do 63 | # exact value depends on system where specs run. 4.0 seems like a safe guess 64 | BW::Device.ios_version.should > '4.0' 65 | end 66 | end 67 | 68 | describe '.orientation' do 69 | it 'delegates to BubbleWrap::Screen.orientation' do 70 | BW::Device.orientation.should == BW::Device::Screen.orientation 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /motion/core/device/ios/camera_wrapper.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module CameraWrapper 4 | 5 | module_function 6 | 7 | # The front-facing camera used to capture media 8 | # @return [Device::Camera, NilClass] a Camera will be returned if there is a front camera, nil otherwise 9 | def front 10 | @front ||= BubbleWrap::Device::Camera.front 11 | end 12 | 13 | # Verifies that the device running has a front facing camera. 14 | # @return [TrueClass, FalseClass] true will be returned if the device has a front facing camera, false otherwise. 15 | def front? 16 | !!front 17 | end 18 | 19 | # The rear-facing camera used to capture media 20 | # @return [Device::Camera, NilClass] a Camera will be returned if there is a rear camera, nil otherwise 21 | def rear 22 | @rear ||= BubbleWrap::Device::Camera.rear 23 | end 24 | 25 | # Verifies that the device running has a rear facing camera. 26 | # @return [TrueClass, FalseClass] true will be returned if the device has a rear facing camera, false otherwise. 27 | def rear? 28 | !!rear 29 | end 30 | 31 | # A Device::Camera used to capture media; by default it will use the :photo_library source type 32 | # See Device::Camera docs for more source type options. 33 | # @return [Device::Camera] a Camera will always be returned. 34 | def any 35 | @any ||= BubbleWrap::Device::Camera.any 36 | end 37 | # alias for any 38 | def photo_library 39 | any 40 | end 41 | 42 | # Should always return true, since picking images from *some* source is always possible 43 | # @return [TrueClass] 44 | def any? 45 | !!any 46 | end 47 | 48 | # Verifies that the device running has a physical camera. 49 | # @return [TrueClass, FalseClass] true will be returned if the device has a physical camera, false otherwise. 50 | def available? 51 | BubbleWrap::Device::Camera.available? 52 | end 53 | end 54 | end 55 | end -------------------------------------------------------------------------------- /spec/motion/font/font_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Font do 2 | [[:system, "systemFontOfSize:"], [:bold, "boldSystemFontOfSize:"], [:italic, "italicSystemFontOfSize:"]].each do |font, method| 3 | describe ".#{font}" do 4 | it "should work" do 5 | f = BubbleWrap::Font.send(font, 16) 6 | f.should == UIFont.send(method, 16) 7 | 8 | f = BubbleWrap::Font.send(font) 9 | f.should == UIFont.send(method, UIFont.systemFontSize) 10 | end 11 | end 12 | end 13 | 14 | describe ".make" do 15 | it "should work with UIFont" do 16 | BubbleWrap::Font.new(UIFont.boldSystemFontOfSize(12)).should == UIFont.boldSystemFontOfSize(12) 17 | end 18 | 19 | it "should work with string" do 20 | BubbleWrap::Font.new("Helvetica").should == UIFont.fontWithName("Helvetica", size: UIFont.systemFontSize) 21 | end 22 | 23 | it "should work with string and size" do 24 | BubbleWrap::Font.new("Helvetica", 18).should == UIFont.fontWithName("Helvetica", size: 18) 25 | end 26 | 27 | it "should work with string and hash" do 28 | BubbleWrap::Font.new("Helvetica", size: 16).should == UIFont.fontWithName("Helvetica", size: 16) 29 | end 30 | 31 | it "should work with hash" do 32 | BubbleWrap::Font.new(name: "Chalkduster", size: 16).should == UIFont.fontWithName("Chalkduster", size: 16) 33 | end 34 | end 35 | 36 | describe ".named" do 37 | it "should work" do 38 | BubbleWrap::Font.named("Helvetica").should == UIFont.fontWithName("Helvetica", size: UIFont.systemFontSize) 39 | end 40 | end 41 | 42 | describe ".attributes" do 43 | it "should work" do 44 | _attributes = BubbleWrap::Font.attributes(font: UIFont.systemFontOfSize(12), color: "red", shadow_color: "blue", shadow_offset: {x: 5, y: 10}) 45 | 46 | _attributes.should == { 47 | UITextAttributeFont => UIFont.systemFontOfSize(12), 48 | UITextAttributeTextColor => UIColor.redColor, 49 | UITextAttributeTextShadowColor => UIColor.blueColor, 50 | UITextAttributeTextShadowOffset => NSValue.valueWithUIOffset(UIOffsetMake(5, 10)) 51 | } 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /spec/motion/ui/ui_activity_view_controller_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::UIActivityViewController do 2 | before do 3 | @controller = UIViewController.alloc.init 4 | @controller.instance_eval do 5 | def presentViewController(*args) 6 | true 7 | end 8 | end 9 | 10 | @options = options = { 11 | item:"BubbleWrap!!!", 12 | animated:false 13 | } 14 | end 15 | 16 | after do 17 | presenting_controller ||= App.window.rootViewController.presentedViewController 18 | presenting_controller ||= App.window.rootViewController 19 | presenting_controller.dismissViewControllerAnimated(false, completion:nil) 20 | end 21 | 22 | it 'Creates an instance of UIActivityViewController' do 23 | activity = BW::UIActivityViewController.new(@options) 24 | 25 | activity.kind_of?(UIActivityViewController).should == true 26 | activity.excludedActivityTypes.should == nil 27 | activity.activityItems.is_a?(Array).should == true 28 | activity.activityItems.count.should == 1 29 | end 30 | 31 | it 'Sets a completion block' do 32 | activity = BW::UIActivityViewController.new(@options) do |activity_type, completed| 33 | test = 2 34 | end 35 | activity.completionHandler.should.not == nil 36 | end 37 | 38 | it 'Sets multiple items' do 39 | options = @options.tap { |o| o.delete(:item) }.merge(items: ["Hello", "BubbleWrap!"]) 40 | 41 | activity = BW::UIActivityViewController.new(options) 42 | activity.activityItems.is_a?(Array).should == true 43 | activity.activityItems.count.should == 2 44 | end 45 | 46 | it 'Sets a single excluded activity' do 47 | activity = BW::UIActivityViewController.new(@options.merge(excluded: :print)) 48 | activity.excludedActivityTypes.is_a?(Array).should == true 49 | activity.excludedActivityTypes.count.should == 1 50 | end 51 | 52 | it 'Sets multiple excluded activities' do 53 | activity = BW::UIActivityViewController.new(@options.merge(excluded: [:print, :add_to_reading_list])) 54 | activity.excludedActivityTypes.is_a?(Array).should == true 55 | activity.excludedActivityTypes.count.should == 2 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /spec/motion/core_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'BubbleWrap' do 2 | 3 | 4 | describe "debug flag" do 5 | 6 | after do 7 | BubbleWrap.debug = false 8 | end 9 | 10 | it "can be set" do 11 | BubbleWrap.debug = true 12 | BubbleWrap.debug?.should.equal true 13 | end 14 | 15 | it "can be unset" do 16 | BubbleWrap.debug = false 17 | BubbleWrap.debug?.should.equal false 18 | end 19 | 20 | end 21 | 22 | describe "RGB color" do 23 | 24 | before do 25 | @red = 23 26 | @green = 45 27 | @blue = 12 28 | end 29 | 30 | it "creates color with rgb devided by 255 with alpha=1" do 31 | r,g,b,a = [(@red/255.0), (@green/255.0), (@blue/255.0), 1] 32 | if App.osx? 33 | color = NSColor.colorWithDeviceRed(r, green:g, blue:b, alpha: a) 34 | else 35 | color = UIColor.colorWithRed(r, green:g, blue:b, alpha:a) 36 | end 37 | BubbleWrap::rgb_color(@red, @green, @blue).should.equal color 38 | end 39 | 40 | it "rgba_color uses the real alpha" do 41 | alpha = 0.4 42 | r,g,b,a = [(@red/255.0), (@green/255.0), (@blue/255.0), alpha] 43 | if App.osx? 44 | color = NSColor.colorWithDeviceRed(r, green:g, blue:b, alpha: a) 45 | else 46 | color = UIColor.colorWithRed(r, green:g, blue:b, alpha:a) 47 | end 48 | BubbleWrap::rgba_color(@red, @green, @blue, alpha).should.equal color 49 | end 50 | 51 | end 52 | 53 | describe "Localized string" do 54 | 55 | it "loads the string from NSBundle" do 56 | key = 'real_key' 57 | value = 'Real Key' 58 | 59 | BubbleWrap::localized_string(key, value).should == value 60 | end 61 | 62 | it "returns the key if localization not found and no value is given" do 63 | key = 'fake_key' 64 | 65 | BubbleWrap::localized_string(key).should == key 66 | end 67 | 68 | end 69 | 70 | describe "uuid" do 71 | 72 | it "creates always the new UUID" do 73 | previous = BW.create_uuid 74 | 10.times do 75 | uuid = BW.create_uuid 76 | uuid.should.not.equal previous 77 | uuid.size.should.equal 36 78 | previous = uuid 79 | end 80 | end 81 | 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /spec/motion/util/deprecated_spec.rb: -------------------------------------------------------------------------------- 1 | module ModuleExample 2 | include BubbleWrap::Deprecated 3 | 4 | module_function 5 | 6 | def a_method 7 | @called = true 8 | end 9 | 10 | deprecated :a_method, "100.0.0" 11 | end 12 | 13 | class ClassExample 14 | include BubbleWrap::Deprecated 15 | 16 | def a_method 17 | @called = true 18 | end 19 | deprecated :a_method, "100.0.0" 20 | end 21 | 22 | module BubbleWrap 23 | def self.set_version(version) 24 | define_singleton_method("version") do 25 | version 26 | end 27 | end 28 | end 29 | 30 | describe BubbleWrap::Deprecated do 31 | describe ".deprecated" do 32 | describe "on a module method" do 33 | describe "with valid version" do 34 | it "should not raise an exception" do 35 | should.not.raise(BubbleWrap::Deprecated::DeprecatedError) { 36 | ModuleExample.a_method 37 | } 38 | end 39 | end 40 | 41 | describe "with invalid version" do 42 | before do 43 | @old_version = BubbleWrap.version 44 | BubbleWrap.set_version("100.0.0") 45 | end 46 | after do 47 | BubbleWrap.set_version(@old_version) 48 | end 49 | 50 | it "should raise an exception" do 51 | should.raise(BubbleWrap::Deprecated::DeprecatedError) { 52 | ModuleExample.a_method 53 | } 54 | end 55 | end 56 | end 57 | 58 | describe "on an instance method" do 59 | describe "with valid version" do 60 | it "should not raise an exception" do 61 | should.not.raise(BubbleWrap::Deprecated::DeprecatedError) { 62 | ClassExample.new.a_method 63 | } 64 | end 65 | end 66 | 67 | describe "with invalid version" do 68 | before do 69 | @old_version = BubbleWrap.version 70 | BubbleWrap.set_version("100.0.0") 71 | end 72 | after do 73 | BubbleWrap.set_version(@old_version) 74 | end 75 | 76 | it "should raise an exception" do 77 | should.raise(BubbleWrap::Deprecated::DeprecatedError) { 78 | ClassExample.new.a_method 79 | } 80 | end 81 | end 82 | end 83 | end 84 | end -------------------------------------------------------------------------------- /samples/osx/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def applicationDidFinishLaunching(notification) 3 | buildMenu 4 | buildWindow 5 | end 6 | 7 | def buildWindow 8 | @mainWindow = NSWindow.alloc.initWithContentRect([[240, 180], [480, 360]], 9 | styleMask: NSTitledWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask|NSResizableWindowMask, 10 | backing: NSBackingStoreBuffered, 11 | defer: false) 12 | @mainWindow.title = "What Is My IP?" 13 | @mainWindow.orderFrontRegardless 14 | 15 | @button = make_button("Find IP") 16 | @button.target = self 17 | @button.action = "fetch_ip" 18 | 19 | @label = make_label("_._._._") 20 | 21 | view = @mainWindow.contentView 22 | view.addSubview(@button) 23 | view.addSubview(@label) 24 | 25 | views_hash = {"button" => @button, "label" => @label} 26 | 27 | add_constraint "|-[button(<=200)]-|", to: view, views: views_hash 28 | 29 | add_constraint "|-[label]-|", to: view, views: views_hash 30 | 31 | add_constraint "V:|-[button]-10-[label]-(>=20,<=60)-|", to: view, views: views_hash 32 | end 33 | 34 | def fetch_ip 35 | @button.title = "Loading" 36 | BW::HTTP.get("http://jsonip.com") do |response| 37 | ip = BW::JSON.parse(response.body.to_str)["ip"] 38 | @label.stringValue = ip 39 | @button.title = "Find IP" 40 | end 41 | end 42 | 43 | def make_button(title) 44 | button = NSButton.alloc.initWithFrame(CGRectZero) 45 | button.title = title 46 | button.buttonType = NSMomentaryLightButton 47 | button.bezelStyle = NSRoundedBezelStyle 48 | button.setTranslatesAutoresizingMaskIntoConstraints(false) 49 | button 50 | end 51 | 52 | def make_label(text) 53 | textField = NSTextField.alloc.initWithFrame(CGRectZero) 54 | textField.stringValue = text 55 | textField.alignment = NSCenterTextAlignment 56 | textField.bezeled = false 57 | textField.drawsBackground = false 58 | textField.editable = false 59 | textField.selectable = false 60 | textField.setTranslatesAutoresizingMaskIntoConstraints(false) 61 | textField 62 | end 63 | 64 | def add_constraint(ascii, params = {}) 65 | view = params[:to] 66 | views_hash = params[:views] 67 | view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(ascii, options: 0, metrics: nil, views: views_hash)) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /samples/alert/app/controllers/alert_view_controller.rb: -------------------------------------------------------------------------------- 1 | class AlertViewController < UIViewController 2 | attr_reader :text_view 3 | attr_reader :buttons 4 | attr_reader :alerts 5 | 6 | def init 7 | super.tap do 8 | @text_view = build_text_view 9 | 10 | @buttons = [] 11 | @alerts = [] 12 | 13 | [:default, :plain_text_input, :secure_text_input, :login_and_password_input].each do |style| 14 | @buttons << build_button(style.to_s) 15 | @alerts << built_alert(style) 16 | end 17 | end 18 | end 19 | 20 | def viewDidLoad 21 | super 22 | 23 | self.view.backgroundColor = UIColor.grayColor 24 | 25 | self.view.addSubview(self.text_view) 26 | 27 | self.buttons.each_with_index do |button, index| 28 | self.view.addSubview(button) 29 | 30 | button.when(UIControlEventTouchUpInside) { self.alerts[index].show } 31 | end 32 | end 33 | 34 | def build_text_view 35 | text_view = UITextView.alloc.initWithFrame([[0, 0], [320, 194]]) 36 | text_view.editable = false 37 | text_view.text = "Waiting..." 38 | text_view 39 | end 40 | 41 | def build_button(title) 42 | button = UIButton.buttonWithType(UIButtonTypeRoundedRect) 43 | button.setTitle(title, forState:UIControlStateNormal) 44 | 45 | rect = self.buttons.empty? ? CGRectMake(20, 150, 280, 44) : self.buttons.last.frame 46 | button.frame = [[rect.origin.x, rect.origin.y + rect.size.height + 20], rect.size] 47 | 48 | button 49 | end 50 | 51 | def built_alert(method) 52 | options = { 53 | :title => method, 54 | :will_present => build_callback(:will_present, method), 55 | :did_present => build_callback(:did_present, method), 56 | :on_click => build_callback(:on_click, method), 57 | :will_dismiss => build_callback(:will_dismiss, method), 58 | :did_dismiss => build_callback(:did_dismiss, method) 59 | } 60 | BW::UIAlertView.send(method, options) 61 | end 62 | 63 | def build_callback(name, method) 64 | lambda do |alert| 65 | message = [] 66 | message << "\n\n" + method.to_s if name == :will_present 67 | message << "\n" + name.to_s 68 | message << "\n" + alert.clicked_button.inspect if alert.clicked_button 69 | 70 | self.text_view.text += message.join 71 | self.text_view.selectedRange = NSMakeRange(self.text_view.text.length, 0) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/lib/bubble-wrap/requirement_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../motion_stub', __FILE__) 2 | require 'bubble-wrap' 3 | 4 | describe BubbleWrap::Requirement do 5 | 6 | before do 7 | @subject = BubbleWrap::Requirement 8 | end 9 | 10 | describe '.scan' do 11 | before do 12 | @subject.clear! 13 | @root_path = File.expand_path('../../../../', __FILE__) 14 | end 15 | 16 | it 'asking for a not-yet-found file raises an exception' do 17 | should.raise(Exception) do 18 | @subject.find('foo') 19 | end 20 | end 21 | 22 | it 'finds the specified file' do 23 | @subject.scan(@root_path, 'motion/core.rb') 24 | @subject.paths.keys.should.include 'motion/core.rb' 25 | end 26 | 27 | it 'finds multiple files according to spec' do 28 | @subject.scan(@root_path, 'motion/**/*.rb') 29 | @subject.files.size.should > 1 30 | end 31 | 32 | it 'never depends on itself' do 33 | @subject.scan(@root_path, 'motion/core.rb') do 34 | file('motion/core.rb').depends_on 'motion/core.rb' 35 | end 36 | @subject.file('motion/core.rb').file_dependencies.should.not.include 'motion/core.rb' 37 | end 38 | 39 | it 'can depend on another file' do 40 | @subject.scan(@root_path, 'motion/*.rb') do 41 | file('motion/http.rb').depends_on('motion/core.rb') 42 | end 43 | @subject.file('motion/http.rb').file_dependencies.should.include @subject.file('motion/core.rb') 44 | end 45 | 46 | it 'can use a framework' do 47 | @subject.scan(@root_path, 'motion/core.rb') do 48 | file('motion/core.rb').uses_framework('FakeFramework') 49 | end 50 | @subject.file('motion/core.rb').frameworks.member?('FakeFramework').should == true 51 | end 52 | 53 | it "figures out the root of the project" do 54 | @subject.scan(File.join(@root_path, 'lib/bubble-wrap.rb'), 'motion/core.rb') 55 | @subject.paths.values.first.root.should == @root_path 56 | end 57 | 58 | describe '.frameworks' do 59 | it 'includes UIKit by default' do 60 | @subject.frameworks.member?('UIKit').should == true 61 | end 62 | 63 | it 'includes Foundation by default' do 64 | @subject.frameworks.member?('Foundation').should == true 65 | end 66 | 67 | it 'includes CoreGraphics by default' do 68 | @subject.frameworks.member?('CoreGraphics').should == true 69 | end 70 | end 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /motion/font/font.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Font 3 | module_function 4 | 5 | def bold(size = nil) 6 | Font.new(:bold, size) 7 | end 8 | 9 | def system(size = nil) 10 | Font.new(:system, size) 11 | end 12 | 13 | def italic(size = nil) 14 | Font.new(:italic, size) 15 | end 16 | 17 | # Example 18 | # Font.new(<# UIFont >) 19 | # Font.new("Helvetica") 20 | # Font.new("Helvetica", 12) 21 | # Font.new("Helvetica", size: 12) 22 | # Font.new(name: "Helvetica", size: 12) 23 | def new(params = {}, *args) 24 | if params.is_a?(UIFont) 25 | return params 26 | end 27 | _font = nil 28 | 29 | if params.is_a?(NSString) 30 | params = {name: params} 31 | end 32 | 33 | if args && !args.empty? 34 | case args[0] 35 | when NSDictionary 36 | params.merge!(args[0]) 37 | else 38 | params.merge!({size: args[0]}) 39 | end 40 | end 41 | params[:size] ||= UIFont.systemFontSize 42 | 43 | case params[:name].to_sym 44 | when :system 45 | _font = UIFont.systemFontOfSize(params[:size].to_f) 46 | when :bold 47 | _font = UIFont.boldSystemFontOfSize(params[:size].to_f) 48 | when :italic 49 | _font = UIFont.italicSystemFontOfSize(params[:size].to_f) 50 | else 51 | begin 52 | _font = UIFont.fontWithName(params[:name], size: params[:size]) 53 | rescue 54 | end 55 | end 56 | 57 | if !_font 58 | raise "Invalid font for parameters: #{params.inspect} args #{args.inspect}" 59 | end 60 | 61 | _font 62 | end 63 | 64 | class << self 65 | alias_method :named, :new 66 | end 67 | 68 | # I.e. for UINavigationBar#titleTextAttributes 69 | def attributes(params = {}) 70 | _attributes = {} 71 | 72 | _attributes[UITextAttributeFont] = Font.new(params[:font]) if params[:font] 73 | _attributes[UITextAttributeTextColor] = params[:color].to_color if params[:color] 74 | _attributes[UITextAttributeTextShadowColor] = params[:shadow_color].to_color if params[:shadow_color] 75 | _attributes[UITextAttributeTextShadowOffset] = begin 76 | x = params[:shadow_offset][:x] 77 | y = params[:shadow_offset][:y] 78 | offset = UIOffsetMake(x,y) 79 | NSValue.valueWithUIOffset(offset) 80 | end if params[:shadow_offset] 81 | 82 | _attributes 83 | end 84 | end 85 | end -------------------------------------------------------------------------------- /spec/motion/rss_parser_spec.rb: -------------------------------------------------------------------------------- 1 | describe "RSSParser" do 2 | 3 | before do 4 | @feed_url = 'https://raw.github.com/gist/2952427/9f1522cbe5d77a72c7c96c4fdb4b77bd58d7681e/atom.xml' 5 | @ns_url = NSURL.alloc.initWithString(@feed_url) 6 | @local_feed = File.join(App.resources_path, 'atom.xml') 7 | end 8 | 9 | describe "initialization" do 10 | 11 | it "works with a string representing an url" do 12 | parser = BW::RSSParser.new(@feed_url) 13 | parser.source.class.should.equal NSURL 14 | parser.source.absoluteString.should.equal @feed_url 15 | end 16 | 17 | it "works with a NSURL instance" do 18 | parser = BW::RSSParser.new(@ns_url) 19 | parser.source.class.should.equal NSURL 20 | parser.source.absoluteString.should.equal @feed_url 21 | end 22 | end 23 | 24 | describe "parsing" do 25 | 26 | it "parses local file data" do 27 | parser = BW::RSSParser.new(File.read(@local_feed).to_data, true) 28 | episodes = [] 29 | parser.parse { |episode| episodes << episode } 30 | episodes.length.should.equal 108 31 | episodes.last.title.should.equal 'Episode 001: Summer of Rails' 32 | end 33 | 34 | it "parses url data" do 35 | parser = BW::RSSParser.new(@feed_url) 36 | episodes = [] 37 | parser.parse { |episode| episodes << episode } 38 | episodes.length.should.equal 108 39 | episodes.last.title.should.equal 'Episode 001: Summer of Rails' 40 | end 41 | 42 | it "handles errors" do 43 | parser = BW::RSSParser.new("http://doesnotexist.com") 44 | parser.parse 45 | parser.state.should.equal :errors 46 | end 47 | 48 | module BW 49 | module HTTP 50 | class << self 51 | # To avoid interfering the http_spec's mocking, we only want to override HTTP.get if it's 52 | # for the RSSParser spec. 53 | alias_method :original_get, :get 54 | def get(url, options = {}, &block) 55 | if url == 'https://raw.github.com/gist/2952427/9f1522cbe5d77a72c7c96c4fdb4b77bd58d7681e/atom.xml' 56 | string = File.read(File.join(App.resources_path, 'atom.xml')) 57 | yield BW::HTTP::Response.new(body: string.to_data, status_code: 200) 58 | elsif url == 'http://doesnotexist.com' 59 | yield BW::HTTP::Response.new(status_code: nil) 60 | else 61 | original_get(url, options, &block) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | end 69 | 70 | -------------------------------------------------------------------------------- /spec/motion/sms/sms_spec.rb: -------------------------------------------------------------------------------- 1 | # Mocking the presentViewController 2 | class MessageViewController < UIViewController 3 | attr_accessor :expectation 4 | 5 | def presentModalViewController(modal, animated: animated) 6 | expectation.call modal, animated 7 | end 8 | end 9 | 10 | # Monkey-patching MFMessageComposeViewController 11 | # So we can access the values that are set. 12 | # This of course breaks MFMessageComposeViewController from actually working, 13 | # but it's testable. 14 | class MFMessageComposeViewController 15 | attr_accessor :recipients, :body 16 | 17 | # for iOS7 compatibility 18 | # on similators, MFMessageComposeViewController.alloc.init returns nil 19 | def init 20 | self 21 | end 22 | end 23 | 24 | describe BW::SMS do 25 | describe ".compose" do 26 | before do 27 | @view_controller = MessageViewController.new 28 | @standard_message_options = { 29 | delegate: @view_controller, 30 | to: [ "1(234)567-8910" ], 31 | message: "This is my message. It isn't very long.", 32 | animated: false 33 | } 34 | end 35 | 36 | it "should open the message controller in a modal" do 37 | @view_controller.expectation = lambda { |message_controller, animated| 38 | message_controller.should.be.kind_of(MFMessageComposeViewController) 39 | } 40 | 41 | BW::SMS.compose @standard_message_options 42 | end 43 | 44 | it "should create a message controller with the right recipient address set" do 45 | @view_controller.expectation = lambda { |message_controller, animated| 46 | message_controller.recipients.should.be.kind_of(Array) 47 | message_controller.recipients.should == @standard_message_options[:to] 48 | } 49 | 50 | BubbleWrap::SMS.compose @standard_message_options 51 | end 52 | 53 | 54 | it "should create a message controller with the right message: set" do 55 | @view_controller.expectation = lambda { |message_controller, animated| 56 | message_controller.body.should.be.kind_of(String) 57 | message_controller.body.should == @standard_message_options[:message] 58 | } 59 | 60 | BubbleWrap::SMS.compose @standard_message_options 61 | end 62 | 63 | it "should create a mail controller with the right animation" do 64 | @view_controller.expectation = lambda { |message_controller, animated| 65 | animated.should.be.false 66 | } 67 | BubbleWrap::SMS.compose @standard_message_options 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /motion/test_suite_delegate.rb: -------------------------------------------------------------------------------- 1 | class TestSuiteDelegate 2 | attr_accessor :window 3 | 4 | def application(application, didFinishLaunchingWithOptions:launchOptions) 5 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 6 | @window.rootViewController = UIViewController.alloc.init 7 | @window.makeKeyAndVisible 8 | true 9 | end 10 | end 11 | 12 | class TestSuiteOSXDelegate 13 | def applicationDidFinishLaunching(notification) 14 | buildMenu 15 | buildWindow 16 | end 17 | 18 | def buildWindow 19 | @mainWindow = NSWindow.alloc.initWithContentRect([[240, 180], [480, 360]], 20 | styleMask: NSTitledWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask|NSResizableWindowMask, 21 | backing: NSBackingStoreBuffered, 22 | defer: false) 23 | @mainWindow.title = "BubbleWrap Tests" 24 | end 25 | 26 | def buildMenu 27 | @mainMenu = NSMenu.new 28 | 29 | appName = "BubbleWrap Tests" 30 | addMenu(appName) do 31 | addItemWithTitle("About #{appName}", action: 'orderFrontStandardAboutPanel:', keyEquivalent: '') 32 | addItem(NSMenuItem.separatorItem) 33 | addItemWithTitle('Preferences', action: 'openPreferences:', keyEquivalent: ',') 34 | addItem(NSMenuItem.separatorItem) 35 | servicesItem = addItemWithTitle('Services', action: nil, keyEquivalent: '') 36 | NSApp.servicesMenu = servicesItem.submenu = NSMenu.new 37 | addItem(NSMenuItem.separatorItem) 38 | addItemWithTitle("Hide #{appName}", action: 'hide:', keyEquivalent: 'h') 39 | item = addItemWithTitle('Hide Others', action: 'hideOtherApplications:', keyEquivalent: 'H') 40 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 41 | addItemWithTitle('Show All', action: 'unhideAllApplications:', keyEquivalent: '') 42 | addItem(NSMenuItem.separatorItem) 43 | addItemWithTitle("Quit #{appName}", action: 'terminate:', keyEquivalent: 'q') 44 | end 45 | 46 | NSApp.helpMenu = addMenu('Help') do 47 | addItemWithTitle("#{appName} Help", action: 'showHelp:', keyEquivalent: '?') 48 | end.menu 49 | 50 | NSApp.mainMenu = @mainMenu 51 | end 52 | 53 | private 54 | 55 | def addMenu(title, &b) 56 | item = createMenu(title, &b) 57 | @mainMenu.addItem item 58 | item 59 | end 60 | 61 | def createMenu(title, &b) 62 | menu = NSMenu.alloc.initWithTitle(title) 63 | menu.instance_eval(&b) if b 64 | item = NSMenuItem.alloc.initWithTitle(title, action: nil, keyEquivalent: '') 65 | item.submenu = menu 66 | item 67 | end 68 | end -------------------------------------------------------------------------------- /spec/motion/ui/ui_control_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe BW::UIControlWrapper do 2 | describe "#when" do 3 | before do 4 | @subject = UIControl.alloc.init 5 | @touched = [] 6 | 7 | @subject.when(UIControlEventTouchUpInside) do 8 | @touched << 'for the very first time' 9 | end 10 | end 11 | 12 | it "supports the 'when' event handler" do 13 | @subject.sendActionsForControlEvents(UIControlEventTouchUpInside) 14 | @touched.should.equal ['for the very first time'] 15 | end 16 | 17 | it "replaces the target for a given control event by default" do 18 | @subject.when(UIControlEventTouchUpInside) do 19 | @touched << 'touched' 20 | end 21 | 22 | @subject.sendActionsForControlEvents(UIControlEventTouchUpInside) 23 | @touched.should.equal ['touched'] 24 | end 25 | 26 | it "BubbleWrap.use_weak_callbacks=true removes cyclic references" do 27 | class ControlSuperView < UIView 28 | def initWithFrame(frame) 29 | super 30 | subject = UIControl.alloc.init 31 | subject.when(UIControlEventTouchUpInside) do 32 | #Can be empty, but we need a block/proc here to potentially create a retain cycle 33 | end 34 | addSubview(subject) 35 | self 36 | end 37 | 38 | def dealloc 39 | App.notification_center.post('ControlSuperView dealloc', nil, {'tag'=>tag}) 40 | super 41 | end 42 | end 43 | 44 | observer = App.notification_center.observe('ControlSuperView dealloc') do |obj| 45 | if obj.userInfo['tag'] == 1 46 | @weak_deallocated = true 47 | elsif obj.userInfo['tag'] == 2 48 | @strong_deallocated = true 49 | end 50 | end 51 | autorelease_pool { 52 | BubbleWrap.use_weak_callbacks = true 53 | v1 = ControlSuperView.new 54 | v1.tag = 1 55 | BubbleWrap.use_weak_callbacks = false 56 | v2 = ControlSuperView.new 57 | v2.tag = 2 58 | } 59 | App.notification_center.unobserve(observer) 60 | @weak_deallocated.should.equal true 61 | @strong_deallocated.should.equal nil 62 | end 63 | 64 | it "allows multiple targets for a given control event if specified" do 65 | @subject.when(UIControlEventTouchUpInside, append: true) do 66 | @touched << 'touched' 67 | end 68 | 69 | @subject.sendActionsForControlEvents(UIControlEventTouchUpInside) 70 | @touched.should.equal ['for the very first time', 'touched'] 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /motion/mail/mail.rb: -------------------------------------------------------------------------------- 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 | options = { 27 | delegate: App.window.rootViewController, 28 | animated: true, 29 | html: false, 30 | to: [], 31 | cc: [], 32 | bcc: [], 33 | subject: 'Contact' 34 | }.merge(options) 35 | 36 | @delegate = options[:delegate] 37 | @mailer_is_animated = options[:animated] 38 | @callback = callback 39 | @callback.weak! if @callback && BubbleWrap.use_weak_callbacks? 40 | 41 | @mail_controller = create_mail_controller(options) 42 | 43 | @delegate.presentViewController(@mail_controller, animated: @mailer_is_animated, completion: options[:completion]) 44 | end 45 | 46 | def create_mail_controller(options = {}) 47 | mail_controller = MFMailComposeViewController.alloc.init 48 | 49 | mail_controller.mailComposeDelegate = self 50 | mail_controller.setToRecipients(Array(options[:to])) 51 | mail_controller.setCcRecipients(Array(options[:cc])) 52 | mail_controller.setBccRecipients(Array(options[:bcc])) 53 | mail_controller.setSubject(options[:subject]) 54 | mail_controller.setMessageBody(options[:message], isHTML: !!options[:html]) 55 | 56 | mail_controller 57 | end 58 | 59 | # Event when the MFMailComposeViewController is closed 60 | # ------------------------------------------------------------- 61 | # the callback is fired if it was present in the constructor 62 | 63 | def mailComposeController(controller, didFinishWithResult: result, error: error) 64 | @delegate.dismissModalViewControllerAnimated(@mailer_is_animated) 65 | @callback.call Result.new(result, error) if @callback 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /motion/ui/ui_view_wrapper.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module UIViewWrapper 3 | def when_tapped(enableInteraction=true, &proc) 4 | add_gesture_recognizer_helper(UITapGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 5 | end 6 | 7 | def when_pinched(enableInteraction=true, &proc) 8 | add_gesture_recognizer_helper(UIPinchGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 9 | end 10 | 11 | def when_rotated(enableInteraction=true, &proc) 12 | add_gesture_recognizer_helper(UIRotationGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 13 | end 14 | 15 | def when_swiped(enableInteraction=true, &proc) 16 | add_gesture_recognizer_helper(UISwipeGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 17 | end 18 | 19 | def when_panned(enableInteraction=true, &proc) 20 | add_gesture_recognizer_helper(UIPanGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 21 | end 22 | 23 | def when_screen_edge_panned(enableInteraction=true, &proc) 24 | add_gesture_recognizer_helper(UIScreenEdgePanGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 25 | end 26 | 27 | def when_pressed(enableInteraction=true, &proc) 28 | add_gesture_recognizer_helper(UILongPressGestureRecognizer.alloc.initWithTarget(self, action:'handle_gesture:'), enableInteraction, proc) 29 | end 30 | 31 | def self.deprecated_methods 32 | %w(whenTapped whenPinched whenRotated whenSwiped whenPanned whenPressed) 33 | end 34 | 35 | deprecated_methods.each do |method| 36 | define_method(method) do |enableInteraction = true, &proc| 37 | NSLog "[DEPRECATED - #{method}] please use #{method.underscore} instead." 38 | send(method.underscore, enableInteraction, &proc) 39 | end 40 | end 41 | 42 | private 43 | 44 | def handle_gesture(recognizer) 45 | @recognizers[recognizer].call(recognizer) 46 | end 47 | 48 | # Adds the recognizer and keeps a strong reference to the Proc object. 49 | def add_gesture_recognizer_helper(recognizer, enableInteraction, proc) 50 | setUserInteractionEnabled true if enableInteraction && !isUserInteractionEnabled 51 | self.addGestureRecognizer(recognizer) 52 | 53 | @recognizers = {} unless @recognizers 54 | proc.weak! if !proc.nil? && BubbleWrap.use_weak_callbacks? 55 | @recognizers[recognizer] = proc 56 | 57 | recognizer 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/bubble-wrap/requirement.rb: -------------------------------------------------------------------------------- 1 | require 'bubble-wrap/requirement/path_manipulation' 2 | 3 | module BubbleWrap 4 | class Requirement 5 | extend PathManipulation 6 | include PathManipulation 7 | 8 | attr_accessor :file, :root 9 | attr_writer :file_dependencies 10 | 11 | def initialize(file,root) 12 | self.file = file 13 | self.root = root 14 | end 15 | 16 | def relative 17 | convert_to_relative(file, root) 18 | end 19 | 20 | def depends_on(file_or_paths) 21 | paths = file_or_paths.respond_to?(:each) ? file_or_paths : [ file_or_paths ] 22 | self.file_dependencies += paths.map do |f| 23 | f = self.class.file(f) unless f.is_a? Requirement 24 | f unless f.file == file 25 | end.compact 26 | self.file_dependencies.uniq!(&:to_s) 27 | end 28 | 29 | def uses_framework(framework_name) 30 | self.frameworks << framework_name 31 | end 32 | 33 | def dependencies 34 | return {} if file_dependencies.empty? 35 | { file => file_dependencies.map(&:to_s) } 36 | end 37 | 38 | def to_s 39 | file 40 | end 41 | 42 | def file_dependencies 43 | @file_dependencies ||= [] 44 | end 45 | 46 | def frameworks 47 | @frameworks ||= [] 48 | end 49 | 50 | class << self 51 | 52 | attr_accessor :paths 53 | 54 | def scan(caller_location, file_spec, &block) 55 | root = convert_caller_to_root_path caller_location 56 | self.paths ||= {} 57 | Dir.glob(File.expand_path(file_spec, root)).each do |file| 58 | p = new(file,root) 59 | self.paths[p.relative] = p 60 | p.depends_on('motion/shortcut.rb') unless p.relative == 'motion/shortcut.rb' 61 | end 62 | self.class_eval(&block) if block 63 | end 64 | 65 | def file(relative) 66 | paths.fetch(relative) 67 | end 68 | 69 | def files(app_files=nil) 70 | files = paths.values.map(&:to_s) 71 | files += app_files if app_files 72 | files.uniq 73 | end 74 | 75 | def clear! 76 | paths.select! { |k,v| v.relative == 'motion/shortcut.rb' } 77 | end 78 | 79 | def files_dependencies 80 | deps = {} 81 | paths.each_value do |file| 82 | deps.merge! file.dependencies 83 | end 84 | deps 85 | end 86 | 87 | def frameworks(app_frameworks=nil) 88 | frameworks = ['Foundation', 'CoreGraphics'] + 89 | paths.values.map(&:frameworks) 90 | frameworks += app_frameworks if app_frameworks 91 | frameworks.flatten.compact.sort.uniq 92 | end 93 | 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/motion/core/time_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Time" do 2 | 3 | describe "Caching the date formatter" do 4 | 5 | it "should reuse the created formatter" do 6 | 100.times do 7 | Time.iso8601("2011-04-11T13:22:21Z") 8 | end 9 | 10 | Thread.current[:date_formatters].count.should.equal 1 11 | Thread.current[:date_formatters]["yyyy-MM-dd'T'HH:mm:ss'Z'"].should.not.equal nil 12 | end 13 | 14 | end 15 | 16 | 17 | describe "parsing an iso8601 formatted time to a Time object" do 18 | before do 19 | @time = Time.iso8601("2012-#{Time.now.month}-#{Time.now.day}T19:41:32Z") 20 | @time_with_timezone = Time.iso8601_with_timezone("1987-08-10T06:00:00+02:00") 21 | @time_with_fractional_seconds = Time.iso8601_with_fractional_seconds("2012-#{Time.now.month}-#{Time.now.day}T19:41:32.123Z") 22 | end 23 | 24 | it "should be a time" do 25 | @time.instance_of?(Time).should == true 26 | @time_with_timezone.instance_of?(Time).should == true 27 | @time_with_fractional_seconds.instance_of?(Time).should == true 28 | end 29 | 30 | # Crashes Buggy RubyMotion 1.18 31 | it "should be converted to the local timezone automatically" do 32 | local_zone = Time.now.zone 33 | @time.zone.should == local_zone 34 | @time_with_timezone.zone == local_zone 35 | @time_with_fractional_seconds.zone.should == local_zone 36 | end 37 | 38 | it "should have a valid year" do 39 | @time.utc.year.should == 2012 40 | @time_with_timezone.utc.year.should == 1987 41 | @time_with_fractional_seconds.utc.year.should == 2012 42 | end 43 | 44 | it "should have a valid month" do 45 | @time.utc.month.should == Time.now.month 46 | @time_with_timezone.utc.month.should == 8 47 | @time_with_fractional_seconds.utc.month.should == Time.now.month 48 | end 49 | 50 | it "should have a valid day" do 51 | @time.utc.day.should == Time.now.day 52 | @time_with_timezone.utc.day.should == 10 53 | @time_with_fractional_seconds.utc.day.should == Time.now.day 54 | end 55 | 56 | it "should have a valid hour" do 57 | @time.utc.hour.should == 19 58 | @time_with_timezone.utc.hour.should == 4 59 | @time_with_fractional_seconds.utc.hour.should == 19 60 | end 61 | 62 | it "should have a valid minute" do 63 | @time.utc.min.should == 41 64 | @time_with_timezone.utc.min.should == 0 65 | @time_with_fractional_seconds.utc.min.should == 41 66 | end 67 | 68 | it "should have a valid second" do 69 | @time.utc.sec.should == 32 70 | @time_with_timezone.utc.sec.should == 0 71 | @time_with_fractional_seconds.utc.sec.should == 32 72 | end 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /spec/motion/core/device/ios/camera_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Device::CameraWrapper do 2 | 3 | before do 4 | BW::Device.camera.instance_variable_set(:@front, nil) 5 | BW::Device.camera.instance_variable_set(:@rear, nil) 6 | end 7 | 8 | describe 'on device with only front facing camera' do 9 | before do 10 | UIImagePickerController.instance_eval do 11 | def isCameraDeviceAvailable(c) 12 | c == UIImagePickerControllerCameraDeviceFront 13 | end 14 | def isSourceTypeAvailable(c) 15 | c == UIImagePickerControllerSourceTypeCamera 16 | end 17 | end 18 | end 19 | 20 | describe '.front?' do 21 | it 'returns true' do 22 | BW::Device.camera.front?.should == true 23 | end 24 | end 25 | 26 | describe '.rear?' do 27 | it 'returns false' do 28 | BW::Device.camera.rear?.should == false 29 | end 30 | end 31 | 32 | describe '.available?' do 33 | it 'returns true' do 34 | BW::Device.camera.available?.should == true 35 | end 36 | end 37 | end 38 | 39 | describe 'on device with only rear facing camera' do 40 | before do 41 | UIImagePickerController.instance_eval do 42 | def isCameraDeviceAvailable(c) 43 | c == UIImagePickerControllerCameraDeviceRear 44 | end 45 | def isSourceTypeAvailable(c) 46 | c == UIImagePickerControllerSourceTypeCamera 47 | end 48 | end 49 | end 50 | 51 | describe '.front?' do 52 | it 'returns false' do 53 | BW::Device.camera.front?.should == false 54 | end 55 | end 56 | 57 | describe '.rear?' do 58 | it 'returns true' do 59 | BW::Device.camera.rear?.should == true 60 | end 61 | end 62 | 63 | describe '.available?' do 64 | it 'returns true' do 65 | BW::Device.camera.available?.should == true 66 | end 67 | end 68 | end 69 | 70 | describe 'on device with no physical camera' do 71 | before do 72 | UIImagePickerController.instance_eval do 73 | def isCameraDeviceAvailable(c) 74 | false 75 | end 76 | def isSourceTypeAvailable(c) 77 | false 78 | end 79 | end 80 | end 81 | 82 | describe '.front?' do 83 | it 'returns false' do 84 | BW::Device.camera.front?.should == false 85 | end 86 | end 87 | 88 | describe '.rear?' do 89 | it 'returns false' do 90 | BW::Device.camera.rear?.should == false 91 | end 92 | end 93 | 94 | describe '.available?' do 95 | it 'returns true' do 96 | BW::Device.camera.available?.should == false 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /motion/core/ios/device.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module_function 4 | 5 | # Verifies that the device running the app is an iPhone. 6 | # @return [TrueClass, FalseClass] true will be returned if the device is an iPhone, false otherwise. 7 | def iphone?(idiom=UIDevice.currentDevice.userInterfaceIdiom) 8 | idiom == UIUserInterfaceIdiomPhone 9 | end 10 | 11 | # Verifies that the device running the app is an iPad. 12 | # @return [TrueClass, FalseClass] true will be returned if the device is an iPad, false otherwise. 13 | def ipad?(idiom=UIDevice.currentDevice.userInterfaceIdiom) 14 | idiom == UIUserInterfaceIdiomPad 15 | end 16 | 17 | # Verifies that the device having a long screen (4 inch iPhone/iPod) 18 | # @return [TrueClass, FalseClass] true will be returned if the device is an iPhone/iPod with 4 inche screen, false otherwise. 19 | def long_screen?(idiom=UIDevice.currentDevice.userInterfaceIdiom, screen_height=UIScreen.mainScreen.bounds.size.height) 20 | iphone?(idiom) && screen_height == 568.0 21 | end 22 | 23 | # Use this to make a DSL-style call for picking images 24 | # @example Device.camera.front 25 | # @return [Device::Camera::CameraWrapper] 26 | def camera 27 | BubbleWrap::Device::CameraWrapper 28 | end 29 | 30 | # Verifies that the device running has a front facing camera. 31 | # @return [TrueClass, FalseClass] true will be returned if the device has a front facing camera, false otherwise. 32 | def front_camera?(picker=UIImagePickerController) 33 | p "This method (front_camera?) is DEPRECATED. Transition to using Device.camera.front?" 34 | picker.isCameraDeviceAvailable(UIImagePickerControllerCameraDeviceFront) 35 | end 36 | 37 | # Verifies that the device running has a rear facing camera. 38 | # @return [TrueClass, FalseClass] true will be returned if the device has a rear facing camera, false otherwise. 39 | def rear_camera?(picker=UIImagePickerController) 40 | p "This method (rear_camera?) is DEPRECATED. Transition to using Device.camera.rear?" 41 | picker.isCameraDeviceAvailable(UIImagePickerControllerCameraDeviceRear) 42 | end 43 | 44 | def simulator? 45 | @simulator_state ||= !(UIDevice.currentDevice.model =~ /simulator/i).nil? 46 | end 47 | 48 | # Returns the IOS SDK version currently running (i.e. "5.1" or "6.0" etc) 49 | # @return [String] the IOS SDK version currently running 50 | def ios_version 51 | UIDevice.currentDevice.systemVersion 52 | end 53 | 54 | # Delegates to BubbleWrap::Screen.orientation 55 | def orientation 56 | screen.orientation 57 | end 58 | 59 | # Delegates to BubbleWrap::Screen.interface_orientation 60 | def interface_orientation 61 | screen.interface_orientation 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /samples/gesture/app/views/drawing/gesture_view.rb: -------------------------------------------------------------------------------- 1 | class GestureView < UIView 2 | attr_accessor :rotation, :scale, :translation 3 | 4 | def initWithCoder(coder) 5 | super 6 | setup 7 | self 8 | end 9 | 10 | def initWithFrame(coder) 11 | super 12 | setup 13 | self 14 | end 15 | 16 | ## UIGestureRecognizerDelegate 17 | 18 | # Note: this method allow rotate and pinch gesture happen at the same time 19 | def gestureRecognizer(recognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer:other_recognizer) 20 | case recognizer 21 | when @rotated_recognizer 22 | if other_recognizer == @pinch_recognizer 23 | return true 24 | end 25 | when @pinch_recognizer 26 | if other_recognizer == @rotated_recognizer 27 | return true 28 | end 29 | end 30 | return false 31 | end 32 | 33 | private 34 | 35 | def setup 36 | self.layer.shouldRasterize = true 37 | self.rotation = 0 38 | self.scale = 1 39 | setup_gesture 40 | end 41 | 42 | def setup_gesture 43 | @panned_recognizer = self.whenPanned do |recognizer| 44 | case(recognizer.state) 45 | when UIGestureRecognizerStateBegan 46 | @last_position = self.position 47 | when UIGestureRecognizerStateChanged 48 | self.translation = recognizer.translationInView(self.superview) 49 | self.position = [@last_position.x + self.translation.x, @last_position.y + self.translation.y] 50 | when UIGestureRecognizerStateEnded 51 | @last_position = nil 52 | end 53 | end 54 | @panned_recognizer.maximumNumberOfTouches = 1 55 | @panned_recognizer.minimumNumberOfTouches = 1 56 | @panned_recognizer.delegate = self 57 | 58 | @rotated_recognizer = self.whenRotated do |recognizer| 59 | case(recognizer.state) 60 | when UIGestureRecognizerStateBegan 61 | @last_rotation = self.rotation 62 | when UIGestureRecognizerStateChanged 63 | self.rotation = @last_rotation + recognizer.rotation 64 | reset_transformation 65 | when UIGestureRecognizerStateEnded 66 | @last_rotation = nil 67 | end 68 | end 69 | @rotated_recognizer.delegate = self 70 | 71 | @pinch_recognizer = self.whenPinched do |recognizer| 72 | case(recognizer.state) 73 | when UIGestureRecognizerStateBegan 74 | @last_scale = self.scale 75 | when UIGestureRecognizerStateChanged 76 | self.scale = @last_scale * recognizer.scale 77 | reset_transformation 78 | when UIGestureRecognizerStateEnded 79 | @last_scale = nil 80 | end 81 | end 82 | @pinch_recognizer.delegate = self 83 | end 84 | 85 | def reset_transformation 86 | transform = CATransform3DIdentity 87 | transform = CATransform3DRotate(transform, -1 * self.rotation, 0.0, 0.0, -1.0) 88 | transform = CATransform3DScale(transform, self.scale, self.scale, 1.0) 89 | self.layer.transform = transform 90 | end 91 | 92 | end -------------------------------------------------------------------------------- /spec/motion/core/app_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::App do 2 | describe '.documents_path' do 3 | it 'should end in "/Documents"' do 4 | App.documents_path[-10..-1].should == '/Documents' 5 | end 6 | end 7 | 8 | describe '.resources_path' do 9 | it 'should end in "/testSuite.app"' do 10 | if App.osx? 11 | App.resources_path.should =~ /\/testSuite(_spec)?.app\/Contents\/Resources$/ 12 | else 13 | App.resources_path.should =~ /\/testSuite(_spec)?.app$/ 14 | end 15 | end 16 | end 17 | 18 | describe '.notification_center' do 19 | it 'should be a NSNotificationCenter' do 20 | App.notification_center.should == NSNotificationCenter.defaultCenter 21 | end 22 | end 23 | 24 | describe '.user_cache' do 25 | it 'should be a NSUserDefaults' do 26 | App.user_cache.should == NSUserDefaults.standardUserDefaults 27 | end 28 | end 29 | 30 | describe '.states' do 31 | it 'returns a hash' do 32 | App.states.class.should == Hash 33 | end 34 | it "returns the real instance variable" do 35 | App.states.should == App.instance_variable_get(:@states) 36 | end 37 | end 38 | 39 | describe '.info_plist' do 40 | it 'returns the information property list hash' do 41 | App.info_plist.should == NSBundle.mainBundle.infoDictionary 42 | end 43 | end 44 | 45 | describe '.name' do 46 | it 'returns the application name' do 47 | App.name.should == 'testSuite' 48 | end 49 | end 50 | 51 | describe '.identifier' do 52 | it 'returns the application identifier' do 53 | App.identifier.should == 'io.bubblewrap.testSuite_spec' 54 | end 55 | end 56 | 57 | describe '.version' do 58 | it 'returns the application version' do 59 | App.version.should == '1.2.3' 60 | end 61 | end 62 | 63 | describe '.run_after' do 64 | class DelayedRunAfterTest; attr_accessor :test_value end 65 | 66 | it 'should run a block after the provided delay' do 67 | @test_obj = DelayedRunAfterTest.new 68 | 69 | App.run_after(0.1){ @test_obj.test_value = true } 70 | wait_for_change(@test_obj, 'test_value') do 71 | @test_obj.test_value.should == true 72 | end 73 | end 74 | 75 | end 76 | 77 | describe ".environment" do 78 | 79 | it 'returns current application environment' do 80 | App.environment.should.equal "test" 81 | end 82 | 83 | end 84 | 85 | describe ".test? .release? .development?" do 86 | 87 | it 'tests if current application environment is test' do 88 | App.test?.should.equal true 89 | end 90 | 91 | it 'tests if current application environment is release' do 92 | App.release?.should.equal false 93 | end 94 | 95 | it 'tests if current application environment is development' do 96 | App.development?.should.equal false 97 | end 98 | 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /motion/core/app.rb: -------------------------------------------------------------------------------- 1 | # Provides a module to store global states 2 | # 3 | module BubbleWrap 4 | module App 5 | include BubbleWrap::Deprecated 6 | 7 | module_function 8 | 9 | # Returns the application's document directory path where users might be able to upload content. 10 | # @return [String] the path to the document directory 11 | def documents_path 12 | NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] 13 | end 14 | 15 | # Returns the application resource path where resource located 16 | # @return [String] the application main bundle resource path 17 | def resources_path 18 | NSBundle.mainBundle.resourcePath 19 | end 20 | 21 | # Returns the default notification center 22 | # @return [NSNotificationCenter] the default notification center 23 | def notification_center 24 | NSNotificationCenter.defaultCenter 25 | end 26 | 27 | def user_cache 28 | NSUserDefaults.standardUserDefaults 29 | end 30 | deprecated :user_cache, "2.0.0" 31 | 32 | # Executes a block after a certain delay 33 | # Usage example: 34 | # App.run_after(0.5) { p "It's #{Time.now}" } 35 | def run_after(delay,&block) 36 | NSTimer.scheduledTimerWithTimeInterval( delay, 37 | target: block, 38 | selector: "call:", 39 | userInfo: nil, 40 | repeats: false) 41 | end 42 | 43 | @states = {} 44 | 45 | def states 46 | @states 47 | end 48 | 49 | def info_plist 50 | NSBundle.mainBundle.infoDictionary 51 | end 52 | 53 | def name 54 | info_plist['CFBundleDisplayName'] 55 | end 56 | 57 | def identifier 58 | NSBundle.mainBundle.bundleIdentifier 59 | end 60 | 61 | def version 62 | info_plist['CFBundleVersion'] 63 | end 64 | 65 | # @return [NSLocale] locale of user settings 66 | def current_locale 67 | languages = NSLocale.preferredLanguages 68 | if languages.count > 0 69 | return NSLocale.alloc.initWithLocaleIdentifier(languages.first) 70 | else 71 | return NSLocale.currentLocale 72 | end 73 | end 74 | 75 | # the current application environment : development, test, release 76 | def environment 77 | RUBYMOTION_ENV 78 | end 79 | 80 | def development? 81 | environment == 'development' 82 | end 83 | 84 | def test? 85 | environment == 'test' 86 | end 87 | 88 | def release? 89 | environment == 'release' 90 | end 91 | 92 | def osx? 93 | Kernel.const_defined?(:NSApplication) 94 | end 95 | 96 | def ios? 97 | Kernel.const_defined?(:UIApplication) 98 | end 99 | end 100 | 101 | end 102 | ::App = BubbleWrap::App unless defined?(::App) 103 | -------------------------------------------------------------------------------- /GEM.md: -------------------------------------------------------------------------------- 1 | # Creating a RubyMotion gem with BubbleWrap 2 | 3 | Let's say we want to develop a simple library gem that lists the 4 | people in a user's addressbook. 5 | 6 | Let's start by initializing an empty gem directory: 7 | 8 | ``` 9 | $ gem install bundler 10 | $ bundle gem bw-addressbook 11 | ``` 12 | 13 | Add BubbleWrap and Rake to your gem's dependencies in `bw-addressbook.gemspec`: 14 | 15 | ```ruby 16 | Gem::Specification.new do |gem| 17 | gem.add_dependency 'bubble-wrap' 18 | gem.add_development_dependency 'rake' 19 | end 20 | ``` 21 | 22 | Then run `bundler`: 23 | ``` 24 | $ bundle 25 | Fetching gem metadata from https://rubygems.org/.. 26 | Using rake (0.9.2.2) 27 | Installing bubble-wrap (0.4.0) 28 | Using bw-addressbook (0.0.1) from source at /Users/jnh/Dev/tmp/bw-addressbook 29 | Using bundler (1.1.4) 30 | Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed. 31 | ``` 32 | 33 | Modify your `lib/bw-addressbook.rb` to include: 34 | 35 | ```ruby 36 | require 'bw-addressbook/version' 37 | BW.require 'motion/address_book.rb' 38 | ``` 39 | 40 | Edit your project's `Rakefile` to include: 41 | 42 | ```ruby 43 | #!/usr/bin/env rake 44 | $:.unshift("/Library/RubyMotion/lib") 45 | require 'motion/project' 46 | require "bundler/gem_tasks" 47 | Bundler.setup 48 | Bundler.require 49 | require 'bubble-wrap/test' 50 | ``` 51 | 52 | At this point we should have a working RubyMotion environment able to 53 | compile our code as we write it. 54 | 55 | Let's start by creating a spec for our address book gem in `spec/address_book_spec.rb`: 56 | 57 | ```ruby 58 | describe AddressBook do 59 | describe '.list' do 60 | it 'returns an Enumerable' do 61 | AddressBook.list.is_a?(Enumerable).should == true 62 | end 63 | end 64 | end 65 | ``` 66 | 67 | Now if you run `rake spec` you can watch the spec fail: 68 | 69 | ``` 70 | 2012-06-07 11:19:35.506 Untitled[14987:f803] *** Terminating app due to uncaught exception 'NameError', reason: 'uninitialized constant AddressBook (NameError)' 71 | *** First throw call stack: 72 | (0x8f6022 0x286cd6 0x140054 0x291f 0x2645 0x1) 73 | terminate called throwing an exception 74 | ``` 75 | 76 | Let's go and define ourselves an `AddressBook` class in `motion/address_book.rb`: 77 | 78 | ```ruby 79 | class AddressBook 80 | end 81 | ``` 82 | 83 | You'll now get a spec failure: 84 | 85 | ``` 86 | NoMethodError: undefined method `list' for AddressBook:Class 87 | spec.rb:156:in `block in run_spec_block': .list - returns an Enumerable 88 | 4:in `execute_block' 89 | spec.rb:156:in `run_spec_block' 90 | spec.rb:171:in `run' 91 | ``` 92 | 93 | Well, we'd better go and define it then, eh? 94 | 95 | ``` 96 | class AddressBook 97 | def self.list 98 | [] 99 | end 100 | end 101 | ``` 102 | 103 | I'm going to leave it here for now, but you're welcome to take a look at the 104 | fully working demonstration project on [Github](http://github.com/jamesotron/bw-addressbook-demo). 105 | -------------------------------------------------------------------------------- /spec/motion/media/player_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Media::Player do 2 | describe ".play" do 3 | before do 4 | @player = BW::Media::Player.new 5 | @local_file = NSURL.fileURLWithPath(File.join(App.resources_path, 'test.mp3')) 6 | end 7 | 8 | it "should raise error if not modal and no callback given" do 9 | should.raise(BW::Media::Error::NilPlayerCallback) do 10 | @player.play(@local_file) 11 | end 12 | end 13 | 14 | it "should yield to a block if not modal" do 15 | @did_yield = false 16 | @player.play(@local_file) do |_player| 17 | @did_yield = true 18 | end 19 | 20 | @did_yield.should == true 21 | @player.stop 22 | end 23 | 24 | it "should use the MPMoviePlayerController option overrides" do 25 | @player.play(@local_file, allows_air_play: true) do |_player| 26 | @did_yield = true 27 | end 28 | @player.media_player.allowsAirPlay.should == true 29 | end 30 | end 31 | 32 | =begin 33 | describe ".play_modal" do 34 | before do 35 | @player = BW::Media::Player.new 36 | @local_file = NSURL.fileURLWithPath(File.join(App.resources_path, 'test.mp3')) 37 | end 38 | 39 | it "should present a modalViewController on root if no controller given" do 40 | @controller = App.window.rootViewController 41 | 42 | @player.play_modal(@local_file) 43 | 44 | 45 | EM.add_timer 2.0 do 46 | resume 47 | end 48 | wait_max 5 do 49 | @controller.modalViewController.should.not == nil 50 | EM.add_timer 4.0 do 51 | resume 52 | end 53 | 54 | @player.stop 55 | wait_max 5 do 56 | @controller.modalViewController.should == nil 57 | @controller = nil 58 | @player = nil 59 | end 60 | end 61 | end 62 | 63 | it "should present a modalViewController if controller given" do 64 | parent = App.window.rootViewController 65 | @controller = UINavigationController.alloc.init 66 | parent.addChildViewController @controller 67 | @controller.viewWillAppear(false) 68 | parent.view.addSubview(@controller.view) 69 | @controller.viewDidAppear(false) 70 | 71 | @controller.didMoveToParentViewController(parent) 72 | 73 | EM.add_timer 3.0 do 74 | resume 75 | end 76 | wait_max 5 do 77 | @player.play_modal(@local_file, controller: @controller) 78 | @controller.modalViewController.should.not == nil 79 | EM.add_timer 2.0 do 80 | @player.stop 81 | end 82 | EM.add_timer 4.0 do 83 | resume 84 | end 85 | 86 | wait_max 5 do 87 | @controller.modalViewController.should == nil 88 | @controller.willMoveToParentViewController(nil) 89 | @controller.viewWillDisappear(false) 90 | @controller.removeFromParentViewController 91 | @controller.viewDidDisappear(false) 92 | @controller = nil 93 | @player = nil 94 | end 95 | end 96 | end 97 | end 98 | =end 99 | end -------------------------------------------------------------------------------- /motion/core/string.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | # This module contains simplified version of the `camelize` and 3 | # `underscore` methods from ActiveSupport, since these are such 4 | # common operations when dealing with the Cocoa API. 5 | module String 6 | 7 | # Convert 'snake_case' into 'CamelCase' 8 | def camelize(uppercase_first_letter = true) 9 | string = self.dup 10 | string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do 11 | new_word = $2.downcase 12 | new_word[0] = new_word[0].upcase 13 | new_word = "/#{new_word}" if $1 == '/' 14 | new_word 15 | end 16 | if uppercase_first_letter && uppercase_first_letter != :lower 17 | string[0] = string[0].upcase 18 | else 19 | string[0] = string[0].downcase 20 | end 21 | string.gsub!('/', '::') 22 | string 23 | end 24 | 25 | # Convert 'CamelCase' into 'snake_case' 26 | def underscore 27 | word = self.dup 28 | word.gsub!(/::/, '/') 29 | word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') 30 | word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') 31 | word.tr!("-", "_") 32 | word.downcase! 33 | word 34 | end 35 | 36 | def to_url_encoded(encoding = KCFStringEncodingUTF8) 37 | encoding = CFStringConvertNSStringEncodingToEncoding(encoding) unless CFStringIsEncodingAvailable(encoding) 38 | CFURLCreateStringByAddingPercentEscapes(nil, self, nil, "!*'();:@&=+$,/?%#[]", encoding) 39 | end 40 | 41 | def to_url_decoded(encoding = nil) 42 | if encoding 43 | encoding = CFStringConvertNSStringEncodingToEncoding(encoding) unless CFStringIsEncodingAvailable(encoding) 44 | CFURLCreateStringByReplacingPercentEscapesUsingEncoding(nil, self, nil, encoding) 45 | else 46 | CFURLCreateStringByReplacingPercentEscapes(nil, self, nil) 47 | end 48 | end 49 | 50 | def to_encoded_data(encoding = NSUTF8StringEncoding) 51 | dataUsingEncoding encoding 52 | end 53 | 54 | def to_color 55 | # First check if it is a color keyword 56 | keyword_selector = "#{self.camelize(:lower)}Color" 57 | color_klass = App.osx? ? NSColor : UIColor 58 | return color_klass.send(keyword_selector) if color_klass.respond_to? keyword_selector 59 | 60 | # Next attempt to convert from hex 61 | hex_color = self.gsub("#", "") 62 | case hex_color.size 63 | when 3 64 | colors = hex_color.scan(%r{[0-9A-Fa-f]}).map!{ |el| (el * 2).to_i(16) } 65 | when 6 66 | colors = hex_color.scan(%r<[0-9A-Fa-f]{2}>).map!{ |el| el.to_i(16) } 67 | when 8 68 | colors = hex_color.scan(%r<[0-9A-Fa-f]{2}>).map!{ |el| el.to_i(16) } 69 | else 70 | raise ArgumentError 71 | end 72 | if colors.size == 3 73 | BubbleWrap.rgb_color(colors[0], colors[1], colors[2]) 74 | elsif colors.size == 4 75 | BubbleWrap.rgba_color(colors[1], colors[2], colors[3], colors[0]) 76 | else 77 | raise ArgumentError 78 | end 79 | end 80 | 81 | end 82 | end 83 | 84 | NSString.send(:include, BubbleWrap::String) 85 | -------------------------------------------------------------------------------- /motion/core/ios/app.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module App 3 | module_function 4 | 5 | # Opens an url (string or instance of `NSURL`) 6 | # in the device's web browser or in the correspondent app for custom schemas 7 | # Usage Example: 8 | # App.open_url("http://matt.aimonetti.net") 9 | # App.open_url("fb://profile") 10 | def open_url(url) 11 | unless url.is_a?(NSURL) 12 | url = NSURL.URLWithString(url) 13 | end 14 | UIApplication.sharedApplication.openURL(url) 15 | end 16 | 17 | # Returns whether an app can open a given URL resource (string or instance of `NSURL`) 18 | # Useful to check if certain apps are installed before calling to their custom schemas. 19 | # Usage Example: 20 | # App.open_url("fb://profile") if App.can_open_url("fb://") 21 | def can_open_url(url) 22 | unless url.is_a?(NSURL) 23 | url = NSURL.URLWithString(url) 24 | end 25 | UIApplication.sharedApplication.canOpenURL(url) 26 | end 27 | 28 | # Displays a UIAlertView. 29 | # 30 | # title - The title as a String. 31 | # args - The title of the cancel button as a String, or a Hash of options. 32 | # (Default: { cancel_button_title: 'OK' }) 33 | # cancel_button_title - The title of the cancel button as a String. 34 | # message - The main message as a String. 35 | # block - Yields the alert object if a block is given, and does so before the alert is shown. 36 | # 37 | # Returns an instance of BW::UIAlertView 38 | def alert(title, *args, &block) 39 | options = { cancel_button_title: 'OK' } 40 | options.merge!(args.pop) if args.last.is_a?(Hash) 41 | 42 | if args.size > 0 && args.first.is_a?(String) 43 | options[:cancel_button_title] = args.shift 44 | end 45 | 46 | options[:title] = title 47 | options[:buttons] = options[:cancel_button_title] 48 | options[:cancel_button_index] = 0 # FIXME: alerts don't have "Cancel" buttons 49 | 50 | alert = UIAlertView.default(options) 51 | 52 | yield(alert) if block_given? 53 | 54 | alert.show 55 | alert 56 | end 57 | 58 | # Return application frame 59 | def frame 60 | UIScreen.mainScreen.applicationFrame 61 | end 62 | 63 | # Main Screen bounds. Useful when starting the app 64 | def bounds 65 | UIScreen.mainScreen.bounds 66 | end 67 | 68 | # Application Delegate 69 | def delegate 70 | UIApplication.sharedApplication.delegate 71 | end 72 | 73 | # the Application object. 74 | def shared 75 | UIApplication.sharedApplication 76 | end 77 | 78 | def windows 79 | UIApplication.sharedApplication.windows 80 | end 81 | 82 | # the Application Window 83 | def window 84 | normal_windows = App.windows.select { |w| 85 | w.windowLevel == UIWindowLevelNormal 86 | } 87 | 88 | key_window = normal_windows.select {|w| 89 | w == UIApplication.sharedApplication.keyWindow 90 | }.first 91 | 92 | key_window || normal_windows.first 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/motion/ui/ui_view_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe BW::UIViewWrapper do 2 | describe "gestures" do 3 | before do 4 | @view = App.delegate.window.rootViewController.view 5 | @orig = @view.isUserInteractionEnabled 6 | @view.setUserInteractionEnabled false 7 | end 8 | 9 | after do 10 | @view.setUserInteractionEnabled @orig 11 | end 12 | 13 | testMethod = proc do |method| 14 | it "returns a gesture recognizer" do 15 | recognizer = @view.send(method, false, &:nil) 16 | recognizer.is_a?(UIGestureRecognizer).should == true 17 | end 18 | 19 | it 'enables interaction when called' do 20 | @view.send(method, &:nil) 21 | @view.isUserInteractionEnabled.should == true 22 | end 23 | 24 | it "doesn't enable interaction if asked not to" do 25 | @view.send(method, false, &:nil) 26 | @view.isUserInteractionEnabled.should == false 27 | end 28 | 29 | # it 'responds to interaction' 30 | end 31 | 32 | describe '#when_tapped' do 33 | testMethod.call :when_tapped 34 | testMethod.call :whenTapped 35 | end 36 | 37 | describe '#when_pinched' do 38 | testMethod.call :when_pinched 39 | testMethod.call :whenPinched 40 | end 41 | 42 | describe '#when_rotated' do 43 | testMethod.call :when_rotated 44 | testMethod.call :whenRotated 45 | end 46 | 47 | describe '#when_swiped' do 48 | testMethod.call :when_swiped 49 | testMethod.call :whenSwiped 50 | end 51 | 52 | describe '#when_panned' do 53 | testMethod.call :when_panned 54 | testMethod.call :whenPanned 55 | end 56 | 57 | describe '#when_screen_edge_panned' do 58 | testMethod.call :when_screen_edge_panned 59 | end 60 | 61 | describe '#when_pressed' do 62 | testMethod.call :when_pressed 63 | testMethod.call :whenPressed 64 | end 65 | 66 | it "BubbleWrap.use_weak_callbacks=true removes cyclic references" do 67 | class ViewSuperView < UIView 68 | def initWithFrame(frame) 69 | super 70 | subject = UIView.alloc.init 71 | subject.when_tapped do 72 | #Can be empty, but we need a block/proc here to potentially create a retain cycle 73 | end 74 | addSubview(subject) 75 | self 76 | end 77 | 78 | def dealloc 79 | App.notification_center.post('ViewSuperView dealloc', nil, {'tag'=>tag}) 80 | super 81 | end 82 | end 83 | 84 | observer = App.notification_center.observe('ViewSuperView dealloc') do |obj| 85 | if obj.userInfo['tag'] == 1 86 | @weak_deallocated = true 87 | elsif obj.userInfo['tag'] == 2 88 | @strong_deallocated = true 89 | end 90 | end 91 | autorelease_pool { 92 | BubbleWrap.use_weak_callbacks = true 93 | v1 = ViewSuperView.new 94 | v1.tag = 1 95 | BubbleWrap.use_weak_callbacks = false 96 | v2 = ViewSuperView.new 97 | v2.tag = 2 98 | } 99 | App.notification_center.unobserve(observer) 100 | @weak_deallocated.should.equal true 101 | @strong_deallocated.should.equal nil 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/motion/core/persistence_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Persistence do 2 | 3 | describe '.app_key' do 4 | 5 | it "caches the @app_key" do 6 | BubbleWrap::Persistence.instance_variable_get(:@app_key).should.equal nil 7 | BubbleWrap::Persistence.app_key 8 | BubbleWrap::Persistence.instance_variable_get(:@app_key).should.not.equal nil 9 | end 10 | 11 | it 'delegates to BubbleWrap::App.idenfitier' do 12 | BubbleWrap::Persistence.app_key.should == BubbleWrap::App.identifier 13 | end 14 | 15 | end 16 | 17 | 18 | describe "storing objects" do 19 | it 'can persist simple objects' do 20 | lambda do 21 | BubbleWrap::Persistence['arbitraryNumber'] = 42 22 | end. 23 | should.not.raise(Exception) 24 | end 25 | 26 | it "must call synchronize" do 27 | storage = NSUserDefaults.standardUserDefaults 28 | def storage.synchronize; @sync_was_called = true; end 29 | 30 | BubbleWrap::Persistence['arbitraryNumber'] = 42 31 | storage.instance_variable_get(:@sync_was_called).should.equal true 32 | end 33 | end 34 | 35 | describe "storing multiple objects" do 36 | it 'can persist multiple objects' do 37 | lambda do 38 | BubbleWrap::Persistence.merge({ 39 | :anotherArbitraryNumber => 9001, 40 | :arbitraryString => 'test string' 41 | }) 42 | end. 43 | should.not.raise(Exception) 44 | end 45 | 46 | it 'must call synchronize' do 47 | storage = NSUserDefaults.standardUserDefaults 48 | def storage.synchronize; @sync_was_called = true; end 49 | 50 | BubbleWrap::Persistence.merge({ 51 | :anotherArbitraryNumber => 9001, 52 | :arbitraryString => 'test string' 53 | }) 54 | storage.instance_variable_get(:@sync_was_called).should.equal true 55 | end 56 | end 57 | 58 | describe "retrieving objects" do 59 | it 'can retrieve persisted objects' do 60 | BubbleWrap::Persistence['arbitraryNumber'].should == 42 61 | BubbleWrap::Persistence[:arbitraryString].should == 'test string' 62 | end 63 | 64 | it 'returns fully functional strings' do 65 | BubbleWrap::Persistence[:arbitraryString].methods.should == 'test string'.methods 66 | end 67 | end 68 | 69 | describe "retrieving all objects" do 70 | it 'can retrieve a dictionary of all objects' do 71 | all = BubbleWrap::Persistence.all 72 | all.is_a?(Hash).should == true 73 | 74 | compare_to = {} 75 | compare_to["anotherArbitraryNumber"] = 9001 76 | compare_to["arbitraryNumber"] = 42 77 | compare_to["arbitraryString"] = "test string" 78 | 79 | all.should == compare_to 80 | end 81 | end 82 | 83 | describe "deleting object" do 84 | before do 85 | BubbleWrap::Persistence['arbitraryString'] = 'foobarbaz' 86 | end 87 | 88 | it 'can delete persisted object' do 89 | BubbleWrap::Persistence.delete(:arbitraryString).should == 'foobarbaz' 90 | BubbleWrap::Persistence['arbitraryString'].should.equal nil 91 | end 92 | 93 | it 'returns nil when the object does not exist' do 94 | BubbleWrap::Persistence.delete(:wrongKey).should == nil 95 | end 96 | 97 | it 'must call synchronize' do 98 | storage = NSUserDefaults.standardUserDefaults 99 | def storage.synchronize; @sync_was_called = true; end 100 | 101 | BubbleWrap::Persistence.delete(:arbitraryString) 102 | 103 | storage.instance_variable_get(:@sync_was_called).should.equal true 104 | end 105 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /motion/core/kvo.rb: -------------------------------------------------------------------------------- 1 | # Usage example: 2 | # 3 | # class ExampleViewController < UIViewController 4 | # include BubbleWrap::KVO 5 | # 6 | # def viewDidLoad 7 | # @label = UILabel.alloc.initWithFrame [[20,20],[280,44]] 8 | # @label.text = "" 9 | # view.addSubview @label 10 | # 11 | # observe(@label, :text) do |old_value, new_value| 12 | # puts "Changed #{old_value} to #{new_value}" 13 | # end 14 | # end 15 | # 16 | # end 17 | # 18 | # @see https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i 19 | module BubbleWrap 20 | module KVO 21 | COLLECTION_OPERATIONS = [ NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement ] 22 | DEFAULT_OPTIONS = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 23 | 24 | def observe(*arguments, &block) 25 | unless [1,2].include?(arguments.length) 26 | raise ArgumentError, "wrong number of arguments (#{arguments.length} for 1 or 2)" 27 | end 28 | 29 | key_path = arguments.pop 30 | target = arguments.pop || self 31 | 32 | target.addObserver(self, forKeyPath:key_path, options:DEFAULT_OPTIONS, context:nil) unless registered?(target, key_path) 33 | add_observer_block(target, key_path, &block) 34 | end 35 | 36 | def unobserve(*arguments) 37 | unless [1,2].include?(arguments.length) 38 | raise ArgumentError, "wrong number of arguments (#{arguments.length} for 1 or 2)" 39 | end 40 | 41 | key_path = arguments.pop 42 | target = arguments.pop || self 43 | 44 | return unless registered?(target, key_path) 45 | 46 | target.removeObserver(self, forKeyPath:key_path) 47 | remove_observer_block(target, key_path) 48 | end 49 | 50 | def unobserve_all 51 | return if @targets.nil? 52 | 53 | @targets.each do |target, key_paths| 54 | key_paths.each_key do |key_path| 55 | target.removeObserver(self, forKeyPath:key_path) 56 | end 57 | end 58 | remove_all_observer_blocks 59 | end 60 | 61 | # Observer blocks 62 | 63 | private 64 | def registered?(target, key_path) 65 | !@targets.nil? && !@targets[target].nil? && @targets[target].has_key?(key_path.to_s) 66 | end 67 | 68 | def add_observer_block(target, key_path, &block) 69 | return if target.nil? || key_path.nil? || block.nil? 70 | 71 | block.weak! if BubbleWrap.use_weak_callbacks? 72 | 73 | @targets ||= {} 74 | @targets[target] ||= {} 75 | @targets[target][key_path.to_s] ||= [] 76 | @targets[target][key_path.to_s] << block 77 | end 78 | 79 | def remove_observer_block(target, key_path) 80 | return if @targets.nil? || target.nil? || key_path.nil? 81 | 82 | key_paths = @targets[target] 83 | key_paths.delete(key_path.to_s) if !key_paths.nil? 84 | end 85 | 86 | def remove_all_observer_blocks 87 | @targets.clear unless @targets.nil? 88 | end 89 | 90 | # NSKeyValueObserving Protocol 91 | 92 | def observeValueForKeyPath(key_path, ofObject: target, change: change, context: context) 93 | key_paths = @targets[target] || {} 94 | blocks = key_paths[key_path] || [] 95 | blocks.each do |block| 96 | args = [change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]] 97 | args << change[NSKeyValueChangeIndexesKey] if collection?(change) 98 | block.call(*args) 99 | end 100 | end 101 | 102 | def collection?(change) 103 | COLLECTION_OPERATIONS.include?(change[NSKeyValueChangeKindKey]) 104 | end 105 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /motion/ui/ui_bar_button_item.rb: -------------------------------------------------------------------------------- 1 | module BW 2 | class UIBarButtonItem < ::UIBarButtonItem 3 | class << self 4 | def styled(type, *objects, &block) 5 | if block.nil? 6 | action = nil 7 | else 8 | block.weak! if BubbleWrap.use_weak_callbacks? 9 | action = :call 10 | end 11 | object = objects.size == 1 ? objects.first : objects 12 | style = Constants.get("UIBarButtonItemStyle", type) 13 | 14 | item = if object.is_a?(String) 15 | alloc.initWithTitle(object, 16 | style:style, 17 | target:block, 18 | action:action 19 | ) 20 | elsif object.is_a?(UIImage) 21 | alloc.initWithImage(object, 22 | style:style, 23 | target:block, 24 | action:action 25 | ) 26 | elsif object.is_a?(Array) && object.size == 2 && object.all? { |o| o.is_a?(UIImage) } 27 | alloc.initWithImage(object[0], 28 | landscapeImagePhone:object[1], 29 | style:style, 30 | target:block, 31 | action:action 32 | ) 33 | else 34 | raise ArgumentError, "invalid object - #{object.inspect}" 35 | end 36 | 37 | item.instance_variable_set(:@target, block) 38 | item 39 | end 40 | 41 | def system(type, &block) 42 | if block.nil? 43 | action = nil 44 | else 45 | block.weak! if BubbleWrap.use_weak_callbacks? 46 | action = :call 47 | end 48 | system_item = Constants.get("UIBarButtonSystemItem", type) 49 | 50 | item = alloc.initWithBarButtonSystemItem(system_item, target:block, action:action) 51 | item.instance_variable_set(:@target, block) 52 | item 53 | end 54 | 55 | def custom(view, &block) 56 | view.when_tapped(true, &block) if block 57 | alloc.initWithCustomView(view) 58 | end 59 | 60 | def new(options = {}, &block) 61 | if options[:styled] 62 | args = options.values_at(:title, :image, :landscape).compact 63 | return styled(options[:styled], *args, &block) 64 | end 65 | 66 | return system(options[:system], &block) if options[:system] 67 | 68 | return custom(options[:custom], &block) if options[:custom] 69 | return custom(options[:view], &block) if options[:view] 70 | 71 | raise ArgumentError, "invalid options - #{options.inspect}" 72 | end 73 | 74 | def build(options = {}, &block) 75 | NSLog "[DEPRECATED - BW::UIBarButtonItem.build] please use .new instead." 76 | new(options, &block) 77 | end 78 | end 79 | end 80 | 81 | Constants.register( 82 | UIBarButtonItemStylePlain, 83 | UIBarButtonItemStyleBordered, 84 | UIBarButtonItemStyleDone, 85 | 86 | UIBarButtonSystemItemDone, 87 | UIBarButtonSystemItemCancel, 88 | UIBarButtonSystemItemEdit, 89 | UIBarButtonSystemItemSave, 90 | UIBarButtonSystemItemAdd, 91 | UIBarButtonSystemItemFlexibleSpace, 92 | UIBarButtonSystemItemFixedSpace, 93 | UIBarButtonSystemItemCompose, 94 | UIBarButtonSystemItemReply, 95 | UIBarButtonSystemItemAction, 96 | UIBarButtonSystemItemOrganize, 97 | UIBarButtonSystemItemBookmarks, 98 | UIBarButtonSystemItemSearch, 99 | UIBarButtonSystemItemRefresh, 100 | UIBarButtonSystemItemStop, 101 | UIBarButtonSystemItemCamera, 102 | UIBarButtonSystemItemTrash, 103 | UIBarButtonSystemItemPlay, 104 | UIBarButtonSystemItemPause, 105 | UIBarButtonSystemItemRewind, 106 | UIBarButtonSystemItemFastForward, 107 | UIBarButtonSystemItemUndo, 108 | UIBarButtonSystemItemRedo, 109 | UIBarButtonSystemItemPageCurl, 110 | ) 111 | end 112 | -------------------------------------------------------------------------------- /GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | # Getting Started with BubbleWrap 2 | 3 | A short guide to starting a RubyMotion application using the bubble wrappers. 4 | 5 | ## A quick note about RubyMotion 6 | 7 | [RubyMotion](http://www.rubymotion.com/) is a commercial product available from 8 | [HipByte SPRL](http://www.hipbyte.com/). RubyMotion needs a recent (10.6 or newer) 9 | version of Mac OS X and Apple's Xcode tools installed. If you don't have a working 10 | RubyMotion install take a look at the [getting started guide](http://www.rubymotion.com/developer-center/guides/getting-started/). 11 | 12 | ## Create a new RubyMotion project 13 | 14 | RubyMotion ships with the `motion` command-line tool to handle creating projects, 15 | updating and creating support tickets. 16 | 17 | ``` 18 | $ motion create bw-demo 19 | Create bw-demo 20 | Create bw-demo/.gitignore 21 | Create bw-demo/Rakefile 22 | Create bw-demo/app 23 | Create bw-demo/app/app_delegate.rb 24 | Create bw-demo/resources 25 | Create bw-demo/spec 26 | Create bw-demo/spec/main_spec.rb 27 | ``` 28 | 29 | This gives us an empty project (and one failing spec). That's fine for now. 30 | 31 | ## Set up Bundler 32 | 33 | [Bundler](http://www.gembundler.com/) is a project to manage your project's 34 | RubyGem dependencies. You can get by without it if you want to, but it will 35 | save time and hassle as the number of BubbleWrap gems grows over time. 36 | 37 | ``` 38 | $ gem install bundler 39 | ``` 40 | 41 | Create a new file called `Gemfile` in your project directory and add the 42 | following: 43 | 44 | ```ruby 45 | source :rubygems 46 | gem 'bubble-wrap', '~> 1.0.0' 47 | gem 'rake' 48 | ``` 49 | 50 | Then run `bundle` from the command-line in the same directory and Bundler 51 | should install BubbleWrap and Rake for you. 52 | 53 | 54 | ## Adding BubbleWrap 55 | 56 | Now that we have BubbleWrap installed we need to configure the project to use 57 | it - the easiest way is to add Bundler to our build-environment and tell it 58 | to take care of everything for us. 59 | 60 | Edit your project's `Rakefile` and add the following just under `require 'motion/project'`: 61 | 62 | ```ruby 63 | require 'bundler' 64 | Bundler.setup 65 | Bundler.require 66 | ``` 67 | 68 | Now when you build your project by running `rake` you will see the BubbleWrap files 69 | being compiled into your project. 70 | 71 | ## Customising BubbleWrap requires 72 | 73 | By default BubbleWrap will include all the bubble wrappers into your project, but often 74 | you won't need them all - perhaps you only want to use BubbleWrap's `http` wrappers? 75 | You can change your bubble-wrap line in your `Gemfile` as follows: 76 | 77 | ```ruby 78 | gem 'bubble-wrap', '~> 1.0.0', :require => 'bubble-wrap/http' 79 | ``` 80 | 81 | If you just want core, the change is similarly easy: 82 | 83 | ```ruby 84 | gem 'bubble-wrap', '~> 1.0.0', :require => 'bubble-wrap/core' 85 | ``` 86 | 87 | Also, if you don't want any of BubbleWrap loaded by default, and you just want to use 88 | it's ability to add projects to your build system you can change it to: 89 | 90 | ```ruby 91 | gem 'bubble-wrap', '~> 1.0.0', :require => 'bubble-wrap/loader' 92 | ``` 93 | 94 | And modify your `Rakefile` to include one or more `BW.require` lines: 95 | 96 | ```ruby 97 | BW.require '/path/to/some/files/**/*.rb' 98 | ``` 99 | 100 | For more information in using `BW.require` take a look at 101 | [the bubblewrap hacking guide](http://bubblewrap.io/hacking.html). 102 | 103 | ## Go forth and conquer! 104 | 105 | The developers wish to thank you for using BubbleWrap. 106 | Please feel free to open issues or pull requests on 107 | [GitHub](https://www.github.com/mattetti/BubbleWrap), join our 108 | [mailing list](https://groups.google.com/forum/#!forum/bubblewrap) 109 | or join us on `#bubblewrap` on `irc.freenode.net`. 110 | -------------------------------------------------------------------------------- /motion/core/device/ios/screen.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Device 3 | module Screen 4 | 5 | module_function 6 | 7 | # Certifies that the device running the app has a Retina display 8 | # @return [TrueClass, FalseClass] true will be returned if the device has a Retina display, false otherwise. 9 | def retina?(screen=UIScreen.mainScreen) 10 | if screen.respondsToSelector('displayLinkWithTarget:selector:') && screen.scale == 2.0 11 | true 12 | else 13 | false 14 | end 15 | end 16 | 17 | # Figure out the current physical orientation of the device 18 | # @return [:portrait, :portrait_upside_down, :landscape_left, :landscape_right, :face_up, :face_down, :unknown] 19 | def orientation(device_orientation=UIDevice.currentDevice.orientation, fallback=true) 20 | case device_orientation 21 | when UIDeviceOrientationPortrait then :portrait 22 | when UIDeviceOrientationPortraitUpsideDown then :portrait_upside_down 23 | when UIDeviceOrientationLandscapeLeft then :landscape_left 24 | when UIDeviceOrientationLandscapeRight then :landscape_right 25 | when UIDeviceOrientationFaceUp then :face_up 26 | when UIDeviceOrientationFaceDown then :face_down 27 | else 28 | # In some cases, the accelerometer can't get an accurate read of orientation so we fall back on the orientation of 29 | # the status bar. 30 | if fallback && (device_orientation != UIApplication.sharedApplication.statusBarOrientation) 31 | orientation(UIApplication.sharedApplication.statusBarOrientation) 32 | else 33 | :unknown 34 | end 35 | end 36 | end 37 | 38 | # Figure out the current orientation of the interface 39 | # @return [:portrait, :portrait_upside_down, :landscape_left, :landscape_right] 40 | def interface_orientation(device_orientation=UIDevice.currentDevice.orientation, fallback=true) 41 | case device_orientation 42 | when UIInterfaceOrientationPortrait then :portrait 43 | when UIInterfaceOrientationPortraitUpsideDown then :portrait_upside_down 44 | when UIInterfaceOrientationLandscapeLeft then :landscape_left 45 | when UIInterfaceOrientationLandscapeRight then :landscape_right 46 | else 47 | # In some cases, the accelerometer can't get an accurate read of orientation so we fall back on the orientation of 48 | # the status bar. 49 | if fallback && (device_orientation != UIApplication.sharedApplication.statusBarOrientation) 50 | orientation(UIApplication.sharedApplication.statusBarOrientation) 51 | else 52 | :unknown 53 | end 54 | end 55 | end 56 | 57 | # The width of the device's screen. 58 | # The real resolution is dependant on the scale 59 | # factor (see `retina?`) but the coordinate system 60 | # is in non-retina pixels. You can get pixel 61 | # accuracy by using half-coordinates. 62 | # This is a Float 63 | def width 64 | UIScreen.mainScreen.bounds.size.width 65 | end 66 | 67 | # The height of the device's screen. 68 | # The real resolution is dependant on the scale 69 | # factor (see `retina?`) but the coordinate system 70 | # is in non-retina pixels. You can get pixel 71 | # accuracy by using half-coordinates. 72 | # This is a Float 73 | def height 74 | UIScreen.mainScreen.bounds.size.height 75 | end 76 | 77 | # The same as `.width` and `.height` but 78 | # compensating for screen rotation (which 79 | # can do your head in). 80 | def width_for_orientation(o=orientation) 81 | return height if (o == :landscape_left) || (o == :landscape_right) 82 | width 83 | end 84 | 85 | # The same as `.width` and `.height` but 86 | # compensating for screen rotation (which 87 | # can do your head in). 88 | def height_for_orientation(o=orientation) 89 | return width if (o == :landscape_left) || (o == :landscape_right) 90 | height 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Hacking on BubbleWrap 2 | 3 | ## A library in two parts 4 | 5 | RubyMotion forces a certain background-radiation of schizophrenia 6 | due to the fact that it's build tools run using the system ruby 7 | via Rake. BubbleWrap manipulates the build environment in order 8 | to make it possible to include itself (and other code) into the 9 | build process from outside the project hierarchy. 10 | 11 | ### Part the first: `lib/` 12 | 13 | This is where [RubyGems](http://rubygems.org) goes looking for 14 | code when you call 15 | 16 | ```ruby 17 | require 'bubble-wrap' 18 | ``` 19 | 20 | When `bubble-wrap` is required it immediately requires `bubble-wrap/loader` 21 | which sets up the infrastructure needed to manipulate the `Rakefile` build process. 22 | Once that is done we can freely call 23 | 24 | ```ruby 25 | BubbleWrap.require 'motion/core/**/*.rb' 26 | ``` 27 | 28 | `BubbleWrap.require` (or simply `BW.require`) is used to include 29 | library code into the Rake build process used by RubyMotion. 30 | `BW.require` is similar to ruby's standard `require` method with 31 | two major changes: 32 | 33 | - it can take a file pattern as used by [`Dir.glob`](http://ruby-doc.org/core-1.9.3/Dir.html#method-c-glob). 34 | - it can be passed a block to manipulate dependencies. 35 | 36 | If a block is passed to `BW.require` it is evaluated in the context 37 | of `BW::Requirement` and thus has access to all it's class methods. 38 | The most common use cases are setting file dependencies: 39 | 40 | ```ruby 41 | BW.require('motion/core/**/*.rb') do 42 | file('motion/core/device/screen.rb').depends_on 'motion/core/device.rb' 43 | end 44 | ``` 45 | 46 | and specifying frameworks that need to be included at build time: 47 | 48 | ```ruby 49 | BW.require('motion/**/*.rb') do 50 | file('motion/address_book.rb').uses_framework 'Addressbook' 51 | end 52 | ``` 53 | 54 | ### Part the second: `motion/` 55 | 56 | Inside the `motion` directory you'll see the actual implementation code 57 | which is compiled into RubyMotion projects that are using BubbleWrap. 58 | 59 | - `motion/core` contains "core" extension, things that the developers 60 | reasonably think should be included in every BubbleWrap using project. 61 | Careful consideration should be taken when making changes to the 62 | contents and test coverage (in `spec/core`) must be updated to match. 63 | This can be included in your project by requiring `bubble-wrap` or 64 | `bubble-wrap/core` in your project `Rakefile`. 65 | - `motion/http` contains the "http" extension. This can be included 66 | by requiring `bubble-wrap/http` in your project `Rakefile`. 67 | - `motion/test_suite_delegate` contains a simple `AppDelegate` which 68 | can be used to enable the `rake spec` to run when developing a 69 | BubbleWrap gem. Using `require 'bubble-wrap/test'` will include 70 | it in the build process and also configure the app delegate to point 71 | to `TestSuiteDelegate`. See the [BubbleWrap gem guide](gem.html) for 72 | more information. 73 | 74 | #### Your project here 75 | 76 | If you think that your project would be of interest to the large number 77 | of RubyMotion users that use BubbleWrap in their daily development then 78 | feel free to fork [the repository on GitHub](https://github.com/mattetti/BubbleWrap) 79 | and send us a pull request. 80 | 81 | You should place your implementation files in a subdirectory of `motion` 82 | (eg `motion/my_awesome_project`), your tests in a subdirectory of `spec` 83 | (eg `spec/my_awesome_project`) and you can create a require file in 84 | `lib/bubble-wrap` for example `lib/bubble-wrap/my_awesome_project.rb`: 85 | 86 | ```ruby 87 | require 'bubble-wrap/loader' 88 | BW.require 'motion/my_awesome_project.rb' 89 | ``` 90 | 91 | People will then be able to use it by adding: 92 | 93 | ```ruby 94 | require 'bubble-wrap/my_awesome_project' 95 | ``` 96 | 97 | to their project's `Rakefile` 98 | 99 | ## Go forth and conquer! 100 | 101 | The developers wish to thank you so much for taking the time 102 | to improve BubbleWrap and by extension the RubyMotion 103 | ecosystem. You're awesome! 104 | -------------------------------------------------------------------------------- /motion/reactor.rb: -------------------------------------------------------------------------------- 1 | module BubbleWrap 2 | module Reactor 3 | module_function 4 | 5 | # Always returns true - for compatibility with EM 6 | def reactor_running? 7 | true 8 | end 9 | alias reactor_thread? reactor_running? 10 | 11 | # Call `callback` or the passed block in `interval` seconds. 12 | # Returns a timer signature that can be passed into 13 | # `cancel_timer` 14 | def add_timer(interval, callback=nil, &blk) 15 | @timers ||= {} 16 | timer = Timer.new(interval,callback,&blk) 17 | timer.on(:fired) do 18 | @timers.delete(timer.object_id) 19 | end 20 | timer.on(:cancelled) do 21 | @timers.delete(timer.object_id) 22 | end 23 | @timers[timer.object_id] = timer 24 | timer.object_id 25 | end 26 | 27 | # Cancel a timer by passing in either a Timer object or 28 | # a timer id (as returned by `add_timer` and 29 | # `add_periodic_timer`). 30 | def cancel_timer(timer) 31 | return timer.cancel if timer.respond_to?(:cancel) 32 | @timers ||= {} 33 | return @timers[timer].cancel if @timers[timer] 34 | false 35 | end 36 | 37 | # Call `callback` or the passed block every `interval` seconds. 38 | # Returns a timer signature that can be passed into 39 | # `cancel_timer` 40 | # Optionally supply a callback as a second argument instead of a block 41 | # (as per EventMachine API) 42 | # Optionally supply :common_modes => true in args to schedule the timer 43 | # for the runloop "common modes" (NSRunLoopCommonModes) instead of 44 | # the default runloop mode. 45 | def add_periodic_timer(interval, *args, &blk) 46 | @timers ||= {} 47 | timer = PeriodicTimer.new(interval,*args,&blk) 48 | timer.on(:cancelled) do 49 | @timers.delete(timer) 50 | end 51 | @timers[timer.object_id] = timer 52 | timer.object_id 53 | end 54 | 55 | # Defer is for integrating blocking operations into the reactor's control 56 | # flow. 57 | # Call defer with one or two blocks, the second block is optional. 58 | # operation = proc do 59 | # # perform a long running operation here 60 | # "result" 61 | # end 62 | # callback = proc do |result| 63 | # # do something with the result here, such as trigger a UI change 64 | # end 65 | # BubbleWrap::Reactor.defer(operation,callback) 66 | # The action of `defer` is to take the block specified in the first 67 | # parameter (the "operation") and schedule it for asynchronous execution 68 | # on a GCD concurrency queue. When the operation completes the result (if any) 69 | # is passed into the callback (if present). 70 | def defer(op=nil,cb=nil,&blk) 71 | schedule do 72 | result = (op||blk).call 73 | schedule(result, &cb) if cb 74 | end 75 | end 76 | 77 | # A version of `defer` which schedules both the operator 78 | # and callback operations on the application's main thread. 79 | def defer_on_main(op=nil,cb=nil,&blk) 80 | schedule_on_main do 81 | result = (op||blk).call 82 | schedule_on_main(result, &cb) if cb 83 | end 84 | end 85 | 86 | # Schedule a block for execution on the reactor queue. 87 | def schedule(*args, &blk) 88 | @queue ||= ::Dispatch::Queue.concurrent("#{NSBundle.mainBundle.bundleIdentifier}.reactor") 89 | blk.weak! if blk && BubbleWrap.use_weak_callbacks? 90 | 91 | cb = proc do 92 | blk.call(*args) 93 | end 94 | @queue.async &cb 95 | nil 96 | end 97 | 98 | # Schedule a block for execution on your application's main thread. 99 | # This is useful as UI updates need to be executed from the main 100 | # thread. 101 | def schedule_on_main(*args, &blk) 102 | blk.weak! if blk && BubbleWrap.use_weak_callbacks? 103 | cb = proc do 104 | blk.call(*args) 105 | end 106 | ::Dispatch::Queue.main.async &cb 107 | end 108 | 109 | end 110 | end 111 | 112 | ::EM = ::BubbleWrap::Reactor unless defined?(::EM) # Yes I dare! 113 | -------------------------------------------------------------------------------- /spec/motion/network-indicator/network_indicator_spec.rb: -------------------------------------------------------------------------------- 1 | describe BW::NetworkIndicator do 2 | 3 | before do 4 | BW::NetworkIndicator.reset! 5 | end 6 | 7 | after do 8 | BW::NetworkIndicator.instance_variable_set(:@counter, 0) 9 | UIApplication.sharedApplication.networkActivityIndicatorVisible = false 10 | end 11 | 12 | it 'should show the indicator immediately' do 13 | BW::NetworkIndicator.show 14 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 15 | end 16 | 17 | it 'should have a counter' do 18 | BW::NetworkIndicator.show 19 | BW::NetworkIndicator.counter.should == 1 20 | BW::NetworkIndicator.hide 21 | BW::NetworkIndicator.counter.should == 0 22 | end 23 | 24 | it 'should show the indicator from any thread' do 25 | Dispatch::Queue.concurrent.async do 26 | BW::NetworkIndicator.show 27 | end 28 | wait BW::NetworkIndicator::DELAY do 29 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 30 | end 31 | end 32 | 33 | it 'should hide the indicator' do 34 | BW::NetworkIndicator.show 35 | BW::NetworkIndicator.hide 36 | wait BW::NetworkIndicator::DELAY do 37 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == false 38 | end 39 | end 40 | 41 | it 'should hide the indicator after a delay' do 42 | BW::NetworkIndicator.show 43 | BW::NetworkIndicator.hide 44 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 45 | wait BW::NetworkIndicator::DELAY do 46 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == false 47 | end 48 | end 49 | 50 | it 'should not hide the indicator if show/hide/show is called quickly' do 51 | BW::NetworkIndicator.show 52 | BW::NetworkIndicator.hide 53 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 54 | wait BW::NetworkIndicator::DELAY/2 do 55 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 56 | BW::NetworkIndicator.show 57 | wait BW::NetworkIndicator::DELAY do 58 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 59 | end 60 | end 61 | end 62 | 63 | it 'should keep track of how many times `show` was called' do 64 | BW::NetworkIndicator.show 65 | BW::NetworkIndicator.show 66 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 67 | BW::NetworkIndicator.hide 68 | wait BW::NetworkIndicator::DELAY do 69 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 70 | BW::NetworkIndicator.hide 71 | wait BW::NetworkIndicator::DELAY do 72 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == false 73 | end 74 | end 75 | end 76 | 77 | it 'should allow `hide` to be called too many times' do 78 | BW::NetworkIndicator.show 79 | BW::NetworkIndicator.show 80 | BW::NetworkIndicator.hide 81 | BW::NetworkIndicator.hide 82 | wait BW::NetworkIndicator::DELAY do 83 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == false 84 | 85 | BW::NetworkIndicator.hide 86 | BW::NetworkIndicator.hide 87 | wait BW::NetworkIndicator::DELAY do 88 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == false 89 | 90 | BW::NetworkIndicator.show 91 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == true 92 | end 93 | end 94 | end 95 | 96 | it 'should reset the counter when `reset!` is called' do 97 | BW::NetworkIndicator.show 98 | BW::NetworkIndicator.show 99 | BW::NetworkIndicator.reset! 100 | UIApplication.sharedApplication.networkActivityIndicatorVisible?.should == false 101 | end 102 | 103 | it 'should have `visible?` method' do 104 | BW::NetworkIndicator.show 105 | BW::NetworkIndicator.visible?.should == true 106 | BW::NetworkIndicator.hide 107 | wait BW::NetworkIndicator::DELAY do 108 | BW::NetworkIndicator.visible?.should == false 109 | end 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /spec/motion/core/json_spec.rb: -------------------------------------------------------------------------------- 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 "doesn't crash when data is nil" do 36 | Proc.new { BW::JSON.parse(nil) }.should.not.raise Exception 37 | end 38 | 39 | it "returns a mutable object" do 40 | Proc.new { @parsed[:blah] = 123 }.should.not.raise Exception 41 | end 42 | 43 | it "should convert a top object into a Ruby hash" do 44 | obj = @parsed 45 | obj.class.should == Hash 46 | obj.keys.size.should == 19 47 | end 48 | 49 | it "should properly convert integers values" do 50 | @parsed["id"].is_a?(Integer).should == true 51 | end 52 | 53 | it "should properly convert string values" do 54 | @parsed["login"].is_a?(String).should == true 55 | end 56 | 57 | it "should convert an array into a Ruby array" do 58 | obj = BubbleWrap::JSON.parse("[1,2,3]") 59 | obj.class.should == Array 60 | obj.size.should == 3 61 | end 62 | 63 | end 64 | 65 | describe "parsing a basic JSON string with block" do 66 | 67 | before do 68 | BubbleWrap::JSON.parse(@json_string) do |parsed| 69 | @parsed = parsed 70 | end 71 | end 72 | 73 | it "should convert a top object into a Ruby hash" do 74 | obj = @parsed 75 | obj.class.should == Hash 76 | obj.keys.size.should == 19 77 | end 78 | 79 | it "should properly convert integers values" do 80 | @parsed["id"].is_a?(Integer).should == true 81 | end 82 | 83 | it "should properly convert string values" do 84 | @parsed["login"].is_a?(String).should == true 85 | end 86 | 87 | it "should convert an array into a Ruby array" do 88 | obj = BubbleWrap::JSON.parse("[1,2,3]") 89 | obj.class.should == Array 90 | obj.size.should == 3 91 | end 92 | 93 | end 94 | 95 | describe "generating a JSON string from an object" do 96 | 97 | before do 98 | @obj = { foo: 'bar', 99 | 'bar' => 'baz', 100 | baz: 123, 101 | foobar: [1,2,3], 102 | foobaz: {'a' => 1, 'b' => 2} 103 | } 104 | end 105 | 106 | it "should generate from a hash" do 107 | json = BubbleWrap::JSON.generate(@obj) 108 | json.class == String 109 | json.should == "{\"foo\":\"bar\",\"bar\":\"baz\",\"baz\":123,\"foobar\":[1,2,3],\"foobaz\":{\"a\":1,\"b\":2}}" 110 | end 111 | 112 | it "should encode and decode and object losslessly" do 113 | json = BubbleWrap::JSON.generate(@obj) 114 | obj = BubbleWrap::JSON.parse(json) 115 | 116 | obj["foo"].should == 'bar' 117 | obj["bar"].should == 'baz' 118 | obj["baz"].should == 123 119 | obj["foobar"].should == [1,2,3] 120 | obj["foobaz"].should == {"a" => 1, "b" => 2} 121 | 122 | # TODO Find out why following line cause runtime error 123 | # obj.keys.sort.should == @obj.keys.sort 124 | # obj.values.sort.should == @obj.values.sort 125 | obj.keys.sort { |a, b| a.to_s <=> b.to_s }.should == @obj.keys.sort { |a, b| a.to_s <=> b.to_s } 126 | obj.values.sort { |a, b| a.to_s <=> b.to_s }.should == @obj.values.sort { |a, b| a.to_s <=> b.to_s } 127 | end 128 | 129 | end 130 | 131 | end 132 | 133 | -------------------------------------------------------------------------------- /spec/motion/mail/mail_spec.rb: -------------------------------------------------------------------------------- 1 | # Mocking the presentViewController 2 | class MailViewController < UIViewController 3 | attr_accessor :expectation 4 | 5 | def presentViewController(modal, animated: animated, completion: completion) 6 | expectation.call modal, animated 7 | end 8 | end 9 | 10 | # Monkey-patching MFMailComposeViewController 11 | # So we can access the values that are set. 12 | # This of course breaks MFMailComposeViewController from actually working, 13 | # but it's testable. 14 | class MFMailComposeViewController 15 | attr_accessor :toRecipients, :ccRecipients, :bccRecipients, :subject, :message, :html 16 | 17 | def setToRecipients(r) 18 | self.toRecipients = r 19 | end 20 | 21 | def setCcRecipients(r) 22 | self.ccRecipients = r 23 | end 24 | 25 | def setBccRecipients(r) 26 | self.bccRecipients = r 27 | end 28 | 29 | def setSubject(r) 30 | self.subject = r 31 | end 32 | 33 | def setMessageBody(message, isHTML: html) 34 | self.message = message 35 | self.html = html 36 | end 37 | end 38 | 39 | describe BW::Mail do 40 | describe ".compose" do 41 | before do 42 | @view_controller = MailViewController.new 43 | @standard_mail_options = { 44 | delegate: @view_controller, 45 | to: [ "tom@example.com" ], 46 | cc: [ "itchy@example.com", "scratchy@example.com" ], 47 | bcc: [ "jerry@example.com" ], 48 | html: false, 49 | subject: "My Subject", 50 | message: "This is my message. It isn't very long.", 51 | animated: false 52 | } 53 | end 54 | 55 | it "should open the mail controller in a modal" do 56 | @view_controller.expectation = lambda { |mail_controller, animated| 57 | mail_controller.should.be.kind_of(MFMailComposeViewController) 58 | } 59 | 60 | BubbleWrap::Mail.compose @standard_mail_options 61 | end 62 | 63 | it "should create a mail controller with the right to: address set" do 64 | @view_controller.expectation = lambda { |mail_controller, animated| 65 | mail_controller.toRecipients.should.be.kind_of(Array) 66 | mail_controller.toRecipients.should == @standard_mail_options[:to] 67 | } 68 | 69 | BubbleWrap::Mail.compose @standard_mail_options 70 | end 71 | 72 | it "should create a mail controller with the right cc: address set" do 73 | @view_controller.expectation = lambda { |mail_controller, animated| 74 | mail_controller.ccRecipients.should.be.kind_of(Array) 75 | mail_controller.ccRecipients.should == @standard_mail_options[:cc] 76 | } 77 | 78 | BubbleWrap::Mail.compose @standard_mail_options 79 | end 80 | 81 | it "should create a mail controller with the right bcc: address set" do 82 | @view_controller.expectation = lambda { |mail_controller, animated| 83 | mail_controller.bccRecipients.should.be.kind_of(Array) 84 | mail_controller.bccRecipients.should == @standard_mail_options[:bcc] 85 | } 86 | 87 | BubbleWrap::Mail.compose @standard_mail_options 88 | end 89 | 90 | it "should create a mail controller with the right subject: set" do 91 | @view_controller.expectation = lambda { |mail_controller, animated| 92 | mail_controller.subject.should.be.kind_of(String) 93 | mail_controller.subject.should == @standard_mail_options[:subject] 94 | } 95 | 96 | BubbleWrap::Mail.compose @standard_mail_options 97 | end 98 | 99 | it "should create a mail controller with the right message: set" do 100 | @view_controller.expectation = lambda { |mail_controller, animated| 101 | mail_controller.message.should.be.kind_of(String) 102 | mail_controller.message.should == @standard_mail_options[:message] 103 | } 104 | 105 | BubbleWrap::Mail.compose @standard_mail_options 106 | end 107 | 108 | it "should create a mail controller with the right html: set" do 109 | @view_controller.expectation = lambda { |mail_controller, animated| 110 | mail_controller.html.should == @standard_mail_options[:html] 111 | } 112 | 113 | BubbleWrap::Mail.compose @standard_mail_options 114 | end 115 | 116 | it "should create a mail controller with the right animation" do 117 | @view_controller.expectation = lambda { |mail_controller, animated| 118 | animated.should.be.false 119 | } 120 | 121 | BubbleWrap::Mail.compose @standard_mail_options 122 | end 123 | 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/lib/bubble-wrap/ext/motion_project_app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'mocha-on-bacon' 2 | require File.expand_path('../../../motion_stub', __FILE__) 3 | require 'bubble-wrap' 4 | 5 | describe BubbleWrap::Ext::BuildTask do 6 | 7 | before do 8 | @subject = Class.new do 9 | def self.setup; end 10 | def self.configs 11 | @configs ||= { :development => Object.new } 12 | end 13 | end 14 | @subject.extend BubbleWrap::Ext::BuildTask 15 | @default_frameworks = ['CoreGraphics', 'Foundation', 'UIKit'] 16 | end 17 | 18 | describe '.extended' do 19 | it 'responds to :setup_with_bubblewrap' do 20 | @subject.respond_to?(:setup_with_bubblewrap).should == true 21 | end 22 | 23 | it 'responds to :setup_without_bubblewrap' do 24 | @subject.respond_to?(:setup_without_bubblewrap).should == true 25 | end 26 | 27 | it 'replaces :setup with :setup_with_bubblewrap' do 28 | @subject.method(:setup).should == @subject.method(:setup_with_bubblewrap) 29 | end 30 | end 31 | 32 | describe '.setup_with_bubblewrap' do 33 | before do 34 | @config = @subject.configs[:development] 35 | @config.stubs(:files=) 36 | @config.stubs(:files) 37 | @config.stubs(:files_dependencies) 38 | @config.stubs(:frameworks) 39 | @config.stubs(:frameworks=) 40 | @subject.stubs(:config).returns(mock()) 41 | @subject.config.stubs(:validate) 42 | end 43 | 44 | it 'calls the passed-in block' do 45 | block = proc { } 46 | block.expects(:call).with(@config) 47 | @subject.setup &block 48 | end 49 | 50 | describe 'when app.files is nil' do 51 | it 'sets app.files' do 52 | @config.stubs(:files).returns(nil) 53 | files = BubbleWrap::Requirement.files 54 | @config.expects(:files=).with(files) 55 | @subject.setup 56 | end 57 | end 58 | 59 | describe 'when app.files is empty' do 60 | it 'sets app.files' do 61 | @config.stubs(:files).returns([]) 62 | files = BubbleWrap::Requirement.files 63 | @config.expects(:files=).with(files) 64 | @subject.setup 65 | end 66 | end 67 | 68 | describe 'when app.files has contents' do 69 | it 'sets app.files' do 70 | mock_files = ['a', 'b', 'c'] 71 | @config.stubs(:files).returns(mock_files) 72 | files = BubbleWrap::Requirement.files + mock_files 73 | @config.expects(:files=).with(files) 74 | @subject.setup 75 | end 76 | end 77 | 78 | it 'removes duplicates from app.files' do 79 | files = ['a', 'a', 'b', 'b', 'c', 'c'] 80 | @config.stubs(:files).returns(files) 81 | @config.expects(:files=).with(BubbleWrap::Requirement.files + files.uniq) 82 | @subject.setup 83 | end 84 | 85 | it 'adds BW dependencies' do 86 | @config.expects(:files_dependencies).with(BubbleWrap::Requirement.files_dependencies) 87 | @subject.setup 88 | end 89 | 90 | describe 'when app.frameworks is empty' do 91 | it 'sets the default frameworks' do 92 | @config.stubs(:frameworks).returns(nil) 93 | @config.expects(:frameworks=).with(@default_frameworks) 94 | @subject.setup 95 | end 96 | end 97 | 98 | describe 'when app.frameworks is empty' do 99 | it 'sets the default frameworks' do 100 | @config.stubs(:frameworks).returns([]) 101 | @config.expects(:frameworks=).with(@default_frameworks) 102 | @subject.setup 103 | end 104 | end 105 | 106 | describe 'when app.frameworks contains defaults' do 107 | it 'sets the default frameworks' do 108 | @config.stubs(:frameworks).returns(@default_frameworks) 109 | @config.expects(:frameworks=).with(@default_frameworks) 110 | @subject.setup 111 | end 112 | end 113 | 114 | describe 'when app.frameworks contains non-defaults' do 115 | it 'sets the default frameworks and the contents' do 116 | @config.stubs(:frameworks).returns(['Addressbook']) 117 | @config.expects(:frameworks=).with(['Addressbook'] + @default_frameworks) 118 | @subject.setup 119 | end 120 | end 121 | 122 | describe 'when BW::Requirement.frameworks has contents' do 123 | it 'sets the default frameworks and the contents' do 124 | BW.require('motion/core.rb') do 125 | file('motion/core.rb').uses_framework('Addressbook') 126 | end 127 | @config.stubs(:frameworks).returns(nil) 128 | @config.expects(:frameworks=).with(['Addressbook'] + @default_frameworks) 129 | @subject.setup 130 | end 131 | end 132 | end 133 | 134 | end 135 | -------------------------------------------------------------------------------- /spec/motion/core/device/ios/camera_spec.rb: -------------------------------------------------------------------------------- 1 | def camera_picker 2 | @camera.instance_variable_get("@picker") 3 | end 4 | 5 | def example_info 6 | { UIImagePickerControllerMediaType => KUTTypeImage, 7 | UIImagePickerControllerOriginalImage => UIImage.alloc.init, 8 | UIImagePickerControllerMediaURL => NSURL.alloc.init} 9 | end 10 | 11 | describe BubbleWrap::Device::Camera do 12 | before do 13 | @controller = UIViewController.alloc.init 14 | @controller.instance_eval do 15 | def presentViewController(*args) 16 | true 17 | end 18 | end 19 | @camera = BW::Device::Camera.new 20 | end 21 | 22 | describe '.flash?' do 23 | before do 24 | UIImagePickerController.instance_eval do 25 | def self.isCameraDeviceAvailable(c) 26 | return true 27 | end 28 | 29 | def self.isFlashAvailableForCameraDevice(c) 30 | return c == UIImagePickerControllerCameraDeviceFront 31 | end 32 | end 33 | end 34 | 35 | it 'should be true for front cameras' do 36 | BW::Device::Camera.front.flash?.should == true 37 | end 38 | 39 | it 'should be false for rear cameras' do 40 | BW::Device::Camera.rear.flash?.should == false 41 | end 42 | end 43 | 44 | describe '.picture' do 45 | it 'should have correct error for source_type camera' do 46 | @camera.picture({source_type: :camera, media_types: [:image]}, @controller) do |result| 47 | result[:error].should == BW::Camera::Error::SOURCE_TYPE_NOT_AVAILABLE 48 | camera_picker.nil?.should == true 49 | end 50 | 51 | @camera.picture({source_type: :saved_photos_album, media_types: [:image]}, @controller) do |result| 52 | result[:error].should == BW::Camera::Error::SOURCE_TYPE_NOT_AVAILABLE 53 | end 54 | end 55 | 56 | describe 'under normal conditions' do 57 | before do 58 | class FakePickerClass 59 | def self.isCameraDeviceAvailable(c) 60 | c == UIImagePickerControllerCameraDeviceFront 61 | end 62 | 63 | def self.isSourceTypeAvailable(c) 64 | c == UIImagePickerControllerSourceTypeCamera 65 | end 66 | 67 | def self.availableMediaTypesForSourceType(c) 68 | [KUTTypeMovie, KUTTypeImage] 69 | end 70 | 71 | def dismissViewControllerAnimated(*args) 72 | true 73 | end 74 | 75 | def self.method_missing(*args) 76 | UIImagePickerController.send(*args) 77 | end 78 | end 79 | @picker_klass = FakePickerClass 80 | end 81 | 82 | it 'should work' do 83 | camera = BW::Device.camera.front 84 | camera.instance_variable_set("@picker_klass", @picker_klass) 85 | image_view = nil 86 | info = example_info 87 | 88 | camera.picture(media_types: [:movie, :image]) do |result| 89 | image_view = UIImageView.alloc.initWithImage(result[:original_image]) 90 | end 91 | 92 | camera.picker 93 | camera.imagePickerController(camera.instance_variable_get("@picker"), didFinishPickingMediaWithInfo: info) 94 | image_view.nil?.should == false 95 | end 96 | 97 | it 'should set popover' do 98 | uiview = UIView.alloc 99 | camera = BW::Device.camera.photo_library.popover_from(uiview) 100 | camera.instance_variable_get("@popover_in_view").should == uiview 101 | end 102 | end 103 | end 104 | 105 | describe '.imagePickerControllerDidCancel' do 106 | it 'should yield the correct error when canceled' do 107 | callback_ran = false 108 | 109 | @camera.picture({source_type: :photo_library, media_types: [:image]}, @controller) do |result| 110 | result[:error].should == BW::Camera::Error::CANCELED 111 | callback_ran = true 112 | end 113 | 114 | @camera.imagePickerControllerDidCancel(camera_picker) 115 | callback_ran.should == true 116 | end 117 | end 118 | 119 | describe '.imagePickerController:didFinishPickingMediaWithInfo:' do 120 | it 'should yield the correct results' do 121 | 122 | info = example_info 123 | callback_ran = false 124 | 125 | @camera.picture({source_type: :photo_library, media_types: [:image]}, @controller) do |result| 126 | result[:error].nil?.should == true 127 | result.keys.should == [:media_type, :original_image, :media_url] 128 | result[:media_type].should == :image 129 | callback_ran = true 130 | end 131 | 132 | @camera.imagePickerController(camera_picker, didFinishPickingMediaWithInfo: info) 133 | callback_ran.should == true 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/motion/reactor_spec.rb: -------------------------------------------------------------------------------- 1 | describe BubbleWrap::Reactor do 2 | before do 3 | @subject = ::BubbleWrap::Reactor 4 | @proxy = Class.new do 5 | attr_accessor :proof 6 | end.new 7 | end 8 | 9 | describe 'slavish EventMachine compatibility' do 10 | describe '.reactor_running?' do 11 | it 'returns true' do 12 | @subject.reactor_running?.should == true 13 | end 14 | end 15 | 16 | describe '.reactor_thread?' do 17 | it 'returns true' do 18 | @subject.reactor_running?.should == true 19 | end 20 | end 21 | end 22 | 23 | describe '.add_timer' do 24 | it 'schedules and executes timers' do 25 | @proxy.proof = false 26 | @subject.add_timer 0.5 do 27 | @proxy.proof = true 28 | end 29 | wait_for_change @proxy, 'proof' do 30 | @proxy.proof.should == true 31 | end 32 | end 33 | 34 | it 'only runs the callback once' do 35 | @proxy.proof = 0 36 | @subject.add_timer 0.1 do 37 | @proxy.proof = @proxy.proof + 1 38 | end 39 | wait 1 do 40 | @proxy.proof.should == 1 41 | end 42 | end 43 | end 44 | 45 | describe '.add_periodic_timer' do 46 | it 'runs callbacks repeatedly' do 47 | @proxy.proof = 0 48 | @timer = @subject.add_periodic_timer 0.5 do 49 | @proxy.proof = @proxy.proof + 1 50 | @subject.cancel_timer(@timer) if @proxy.proof > 2 51 | end 52 | wait 1.1 do 53 | @proxy.proof.should >= 2 54 | end 55 | end 56 | 57 | it 'accepts non-block callbacks' do 58 | @proxy.proof = 0 59 | callback = lambda { 60 | @proxy.proof = @proxy.proof + 1 61 | @subject.cancel_timer(@timer) if @proxy.proof > 2 62 | } 63 | @timer = @subject.add_periodic_timer 0.5, callback 64 | wait 1.1 do 65 | @proxy.proof.should >= 2 66 | end 67 | end 68 | end 69 | 70 | describe '.cancel_timer' do 71 | it 'cancels timers' do 72 | @proxy.proof = true 73 | timer = @subject.add_timer 10.0 do 74 | @proxy.proof = false 75 | end 76 | @subject.cancel_timer(timer) 77 | @proxy.proof.should == true 78 | end 79 | 80 | it 'cancels periodic timers' do 81 | @proxy.proof = true 82 | timer = @subject.add_periodic_timer 10.0 do 83 | @proxy.proof = false 84 | end 85 | @subject.cancel_timer(timer) 86 | @proxy.proof.should == true 87 | end 88 | end 89 | 90 | describe '.defer' do 91 | it 'defers the operation' do 92 | @proxy.proof = false 93 | @subject.defer do 94 | @proxy.proof = true 95 | end 96 | @proxy.proof.should == false 97 | wait 0.5 do 98 | @proxy.proof.should == true 99 | end 100 | end 101 | 102 | it 'calls the callback after the operation finishes' do 103 | @proxy.proof = false 104 | cb = proc do |result| 105 | @proxy.proof = result 106 | end 107 | op = proc do 108 | true 109 | end 110 | @subject.defer(op,cb) 111 | wait 0.5 do 112 | @proxy.proof.should == true 113 | end 114 | end 115 | end 116 | 117 | describe '.schedule' do 118 | it 'defers the operation' do 119 | @proxy.proof = false 120 | @subject.schedule do 121 | @proxy.proof = true 122 | end 123 | @proxy.proof.should == false 124 | wait 0.5 do 125 | @proxy.proof.should == true 126 | end 127 | end 128 | 129 | # I wish these specs would run, but they kill RubyMotion. *sad face* 130 | 131 | # it 'runs the operation on the reactor queue' do 132 | # @proxy.proof = false 133 | # @subject.schedule do 134 | # @proxy.proof = ::Reactor::Queue.current.to_s 135 | # end 136 | # wait 0.75 do 137 | # @proxy.proof.should == "#{NSBundle.mainBundle.bundleIdentifier}.reactor" 138 | # end 139 | # end 140 | 141 | # it 'runs the callback on the main queue' do 142 | # @proxy.proof = false 143 | # @subject.schedule do 144 | # @proxy.proof = ::Reactor::Queue.current.to_s 145 | # end 146 | # wait 0.75 do 147 | # @proxy.proof.should == ::Reactor::Queue.main.to_s 148 | # end 149 | # end 150 | end 151 | 152 | describe '.schedule_on_main' do 153 | it 'defers the operation' do 154 | @proxy.proof = false 155 | @subject.schedule do 156 | @proxy.proof = true 157 | end 158 | @proxy.proof.should == false 159 | wait 0.5 do 160 | @proxy.proof.should == true 161 | end 162 | end 163 | 164 | # it 'runs the operation on the main queue' do 165 | # @proxy.proof = false 166 | # @subject.schedule do 167 | # @proxy.proof = ::Reactor::Queue.current.to_s 168 | # end 169 | # wait 0.75 do 170 | # @proxy.proof.should == ::Reactor::Queue.main.to_s 171 | # end 172 | # end 173 | end 174 | 175 | end 176 | -------------------------------------------------------------------------------- /motion/rss_parser.rb: -------------------------------------------------------------------------------- 1 | # Asynchronous and non blocking RSS Parser based on NSXMLParser. 2 | # The parser will progressively parse a feed and yield each item to the provided block. 3 | # 4 | # The parser can also trigger delegate methods, you can define the following delegate method on the receiving object: 5 | # def when_parser_initializes; end 6 | # def when_parser_parses; end 7 | # def when_parser_is_done; end 8 | # def when_parser_errors; end 9 | # 10 | # @see https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSXMLParser_Class/Reference/Reference.html 11 | # 12 | # @usage example: 13 | # feed = RSSParser.new(URL) 14 | # feed.delegate = self 15 | # feed.parse do |item| 16 | # print item.link 17 | # end 18 | # def when_parser_is_done 19 | # App.alert('parsing complete') 20 | # end 21 | # 22 | module BubbleWrap 23 | class RSSParser 24 | 25 | attr_accessor :parser, :source, :doc, :debug, :delegate, :parser_error 26 | attr_reader :state 27 | 28 | # RSSItem is a simple class that holds all of RSS items. 29 | # Extend this class to display/process the item differently. 30 | class RSSItem 31 | attr_accessor :title, :description, :link, :guid, :pubDate, :enclosure 32 | 33 | def initialize 34 | @title, @description, @link, @pubDate, @guid = '', '', '', '', '' 35 | end 36 | 37 | def to_hash 38 | { 39 | :title => title, 40 | :description => description, 41 | :link => link, 42 | :pubDate => pubDate, 43 | :guid => guid, 44 | :enclosure => enclosure 45 | } 46 | end 47 | end 48 | 49 | def initialize(input, data=false) 50 | if data 51 | data_to_parse = input.respond_to?(:to_data) ? input.to_data : input 52 | @source = data_to_parse 53 | else 54 | url = input.is_a?(NSURL) ? input : NSURL.alloc.initWithString(input) 55 | @source = url 56 | end 57 | self.state = :initializes 58 | self 59 | end 60 | 61 | def state=(new_state) 62 | @state = new_state 63 | callback_meth = "when_parser_#{new_state}" 64 | if self.delegate && self.delegate.respond_to?(callback_meth) 65 | self.delegate.send(callback_meth) 66 | end 67 | end 68 | 69 | # Starts the parsing and send each parsed item through its block. 70 | # 71 | # Usage: 72 | # feed.parse do |item| 73 | # puts item.link 74 | # end 75 | def parse(&block) 76 | @block = block 77 | 78 | fetch_source_data do |data| 79 | @parser = NSXMLParser.alloc.initWithData(data) 80 | @parser.shouldProcessNamespaces = true 81 | @parser.delegate ||= self 82 | @parser.parse 83 | end 84 | end 85 | 86 | # Delegate getting called when parsing starts 87 | def parserDidStartDocument(parser) 88 | puts "starting parsing.." if debug 89 | self.state = :parses 90 | end 91 | 92 | # Delegate being called when an element starts being processed 93 | def parser(parser, didStartElement:element, namespaceURI:uri, qualifiedName:name, attributes:attrs) 94 | if element == 'item' 95 | @current_item = RSSItem.new 96 | elsif element == 'enclosure' 97 | @current_item.enclosure = attrs 98 | end 99 | @current_element = element 100 | end 101 | 102 | # as the parser finds characters, this method is being called 103 | def parser(parser, foundCharacters:string) 104 | if @current_element && @current_item && @current_item.respond_to?(@current_element) 105 | el = @current_item.send(@current_element) 106 | el << string if el.respond_to?(:<<) 107 | end 108 | end 109 | 110 | # method called when an element is done being parsed 111 | def parser(parser, didEndElement:element, namespaceURI:uri, qualifiedName:name) 112 | if element == 'item' 113 | @block.call(@current_item) if @block 114 | else 115 | @current_element = nil 116 | end 117 | end 118 | 119 | # method called when the parser encounters an error 120 | # error can be retrieved with parserError 121 | def parser(parser, parseErrorOccurred:parse_error) 122 | puts "parseErrorOccurred" if debug 123 | @parser_error = parse_error 124 | 125 | self.state = :errors 126 | end 127 | 128 | # delegate getting called when the parsing is done 129 | # If a block was set, it will be called on each parsed items 130 | def parserDidEndDocument(parser) 131 | puts "done parsing" if debug 132 | self.state = :is_done 133 | end 134 | 135 | def parserError 136 | @parser_error || @parser.parserError 137 | end 138 | 139 | # TODO: implement 140 | # parser:validationErrorOccurred: 141 | # parser:foundCDATA: 142 | 143 | protected 144 | 145 | def fetch_source_data(&blk) 146 | if @source.is_a?(NSURL) 147 | HTTP.get(@source.absoluteString) do |response| 148 | if response.ok? 149 | blk.call(response.body) 150 | else 151 | parser(parser, parseErrorOccurred:"HTTP request failed (#{response})") 152 | end 153 | end 154 | else 155 | yield @source 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /samples/osx/app/menu.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | def buildMenu 3 | @mainMenu = NSMenu.new 4 | 5 | appName = NSBundle.mainBundle.infoDictionary['CFBundleName'] 6 | addMenu(appName) do 7 | addItemWithTitle("About #{appName}", action: 'orderFrontStandardAboutPanel:', keyEquivalent: '') 8 | addItem(NSMenuItem.separatorItem) 9 | addItemWithTitle('Preferences', action: 'openPreferences:', keyEquivalent: ',') 10 | addItem(NSMenuItem.separatorItem) 11 | servicesItem = addItemWithTitle('Services', action: nil, keyEquivalent: '') 12 | NSApp.servicesMenu = servicesItem.submenu = NSMenu.new 13 | addItem(NSMenuItem.separatorItem) 14 | addItemWithTitle("Hide #{appName}", action: 'hide:', keyEquivalent: 'h') 15 | item = addItemWithTitle('Hide Others', action: 'hideOtherApplications:', keyEquivalent: 'H') 16 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 17 | addItemWithTitle('Show All', action: 'unhideAllApplications:', keyEquivalent: '') 18 | addItem(NSMenuItem.separatorItem) 19 | addItemWithTitle("Quit #{appName}", action: 'terminate:', keyEquivalent: 'q') 20 | end 21 | 22 | addMenu('File') do 23 | addItemWithTitle('New', action: 'newDocument:', keyEquivalent: 'n') 24 | addItemWithTitle('Open…', action: 'openDocument:', keyEquivalent: 'o') 25 | addItem(NSMenuItem.separatorItem) 26 | addItemWithTitle('Close', action: 'performClose:', keyEquivalent: 'w') 27 | addItemWithTitle('Save…', action: 'saveDocument:', keyEquivalent: 's') 28 | addItemWithTitle('Revert to Saved', action: 'revertDocumentToSaved:', keyEquivalent: '') 29 | addItem(NSMenuItem.separatorItem) 30 | addItemWithTitle('Page Setup…', action: 'runPageLayout:', keyEquivalent: 'P') 31 | addItemWithTitle('Print…', action: 'printDocument:', keyEquivalent: 'p') 32 | end 33 | 34 | addMenu('Edit') do 35 | addItemWithTitle('Undo', action: 'undo:', keyEquivalent: 'z') 36 | addItemWithTitle('Redo', action: 'redo:', keyEquivalent: 'Z') 37 | addItem(NSMenuItem.separatorItem) 38 | addItemWithTitle('Cut', action: 'cut:', keyEquivalent: 'x') 39 | addItemWithTitle('Copy', action: 'copy:', keyEquivalent: 'c') 40 | addItemWithTitle('Paste', action: 'paste:', keyEquivalent: 'v') 41 | item = addItemWithTitle('Paste and Match Style', action: 'pasteAsPlainText:', keyEquivalent: 'V') 42 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 43 | addItemWithTitle('Delete', action: 'delete:', keyEquivalent: '') 44 | addItemWithTitle('Select All', action: 'selectAll:', keyEquivalent: 'a') 45 | end 46 | 47 | fontMenu = createMenu('Font') do 48 | addItemWithTitle('Show Fonts', action: 'orderFrontFontPanel:', keyEquivalent: 't') 49 | addItemWithTitle('Bold', action: 'addFontTrait:', keyEquivalent: 'b') 50 | addItemWithTitle('Italic', action: 'addFontTrait:', keyEquivalent: 'b') 51 | addItemWithTitle('Underline', action: 'underline:', keyEquivalent: 'u') 52 | addItem(NSMenuItem.separatorItem) 53 | addItemWithTitle('Bigger', action: 'modifyFont:', keyEquivalent: '+') 54 | addItemWithTitle('Smaller', action: 'modifyFont:', keyEquivalent: '-') 55 | end 56 | 57 | textMenu = createMenu('Text') do 58 | addItemWithTitle('Align Left', action: 'alignLeft:', keyEquivalent: '{') 59 | addItemWithTitle('Center', action: 'alignCenter:', keyEquivalent: '|') 60 | addItemWithTitle('Justify', action: 'alignJustified:', keyEquivalent: '') 61 | addItemWithTitle('Align Right', action: 'alignRight:', keyEquivalent: '}') 62 | addItem(NSMenuItem.separatorItem) 63 | addItemWithTitle('Show Ruler', action: 'toggleRuler:', keyEquivalent: '') 64 | addItemWithTitle('Copy Ruler', action: 'copyRuler:', keyEquivalent: 'c') 65 | addItemWithTitle('Paste Ruler', action: 'pasteRuler:', keyEquivalent: 'v') 66 | end 67 | 68 | addMenu('Format') do 69 | addItem fontMenu 70 | addItem textMenu 71 | end 72 | 73 | addMenu('View') do 74 | item = addItemWithTitle('Show Toolbar', action: 'toggleToolbarShown:', keyEquivalent: 't') 75 | item.keyEquivalentModifierMask = NSCommandKeyMask|NSAlternateKeyMask 76 | addItemWithTitle('Customize Toolbar…', action: 'runToolbarCustomizationPalette:', keyEquivalent: '') 77 | end 78 | 79 | NSApp.windowsMenu = addMenu('Window') do 80 | addItemWithTitle('Minimize', action: 'performMiniaturize:', keyEquivalent: 'm') 81 | addItemWithTitle('Zoom', action: 'performZoom:', keyEquivalent: '') 82 | addItem(NSMenuItem.separatorItem) 83 | addItemWithTitle('Bring All To Front', action: 'arrangeInFront:', keyEquivalent: '') 84 | end.menu 85 | 86 | NSApp.helpMenu = addMenu('Help') do 87 | addItemWithTitle("#{appName} Help", action: 'showHelp:', keyEquivalent: '?') 88 | end.menu 89 | 90 | NSApp.mainMenu = @mainMenu 91 | end 92 | 93 | private 94 | 95 | def addMenu(title, &b) 96 | item = createMenu(title, &b) 97 | @mainMenu.addItem item 98 | item 99 | end 100 | 101 | def createMenu(title, &b) 102 | menu = NSMenu.alloc.initWithTitle(title) 103 | menu.instance_eval(&b) if b 104 | item = NSMenuItem.alloc.initWithTitle(title, action: nil, keyEquivalent: '') 105 | item.submenu = menu 106 | item 107 | end 108 | end 109 | --------------------------------------------------------------------------------