Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lessons Learned: Building an iOS Application wi...

Lessons Learned: Building an iOS Application with RubyMotion

Let's explore practices and solutions derived from the development of dscovr, a greenfield RubyMotion project a year in the making. Devon will share the lessons learned, and what worked best for his team, during the process of building an app with RubyMotion from concept to launch. Based primarily from the perspective of a Ruby on Rails developer, we'll cover tips on workflow, working with designers, continuous integration, and more!

Devon Blandin is a full-stack developer at dscout in Chicago, IL. He successfully took a new iOS app from concept to launch using the RubyMotion toolchain.

Avatar for Devon Blandin

Devon Blandin

May 21, 2014
Tweet

More Decks by Devon Blandin

Other Decks in Programming

Transcript

  1. Michael Kiser Good Beer Hunting Brandy Shultz Good Beer Hunting

    Victoria McGinley Designer & Blogger Eve Turow Writer & Chef Kiki Luthringshausen Food & Lifestyle Writer Max Wastler Writer & Editor Meg Biram Blogger Hitha Palepu Travel Expert Barri Leiner Grant Stylist
  2. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  3. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  4. Rake in RubyMotion rake archive # Create an .ipa archive!

    rake archive:distribution # Create an .ipa archive for distribution...! rake build # Build everything! rake build:device # Build the device version! rake build:simulator # Build the simulator version! rake clean # Clear local build objects! rake clean:all # Clean all build objects! rake config # Show project config! rake crashlog # Same as crashlog:simulator! rake crashlog:device # Retrieve and symbolicate crash logs gen...! rake crashlog:simulator # Open the latest crash report generated ...! rake ctags # Generate ctags! rake default # Build the project, then run the simulator! rake device # Deploy on the device! rake profile # Same as profile:simulator! rake profile:device # Run a build on the device through Instr...! rake profile:device:templates # List all built-in device Instruments te...! rake profile:simulator # Run a build on the simulator through In...! rake profile:simulator:templates # List all built-in Simulator Instruments...! rake simulator # Run the simulator! rake spec # Same as 'spec:simulator'! rake spec:device # Run the test/spec suite on the device! rake spec:simulator # Run the test/spec suite on the simulator! rake static # Create a .a static library!
  5. Deploying via Rake desc 'Deploy to TestFlight/Crittercism'! task :deploy do!

    %w[slack:starting deploy:archive testflight:upload crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! $ rake deploy!
  6. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  7. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  8. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  9. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  10. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  11. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  12. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  13. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  14. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  15. desc 'Deploy to TestFlight/Crittercism'! task :deploy do! %w[slack:starting deploy:archive testflight:upload

    crittercism:upload slack:finished].each do |task|! Rake::Task[task].invoke! ! raise "#{task} failed!" if $?.exitstatus.nonzero?! end! end! Deploying via Rake $ rake deploy!
  16. Deploying via Rake $ rake deploy! "Sending Slack 'Deploying' notification..."!

    "Preparing archive..."! Create ./resources/Settings.bundle/Root.plist! "Setting testflight options..."! Build ./build/iPhoneOS-7.0-Development! Build...! Compile ...! Link...! Link ./build/iPhoneOS-7.0-Development/dscovr.app/dscovr! Create ./build/iPhoneOS-7.0-Development/dscovr.app/Info.plist! Copy ...! Create ./build/iPhoneOS-7.0-Development/dscovr.dSYM! Create ./build/iPhoneOS-7.0-Development/dscovr.app/embedded.mobileprovision! Codesign ./build/iPhoneOS-7.0-Development/dscovr.app! Create ./build/iPhoneOS-7.0-Development/dscovr.ipa! "Zipping .dSYM file..."! "Submitting to TestFlight..."! "TestFlight: 200 OK"! "Submitting to Crittercism..."! "Crittercism: 200 OK"! "Sending Slack 'Deployed' notification..."!
  17. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  18. Localized Strings tkadauke/motion-i18n rake translate # Convert YAML translations to

    Localizable.strings format! rake translations:merge # Merge in new translations! rake translations:missing # Detect missing translations! rake translations:unused # Detect translations that are not used in code!
  19. Localized Strings tkadauke/motion-i18n def description_label! @description_label ||= UILabel.new.tap do |label|!

    label.text = ‘You can invite up to 10 of your friends!’! label.textAlignment = NSTextAlignmentCenter! label.numberOfLines = 0! end! end!
  20. Localized Strings tkadauke/motion-i18n def description_label! @description_label ||= UILabel.new.tap do |label|!

    label.text = I18n.t('invites.description')! label.textAlignment = NSTextAlignmentCenter! label.numberOfLines = 0! end! end!
  21. Localized Strings config/locales/en.yml en:! events:! like: "%{name} liked your discovery!"!

    comment: "%{name} commented on your discovery!"! comment_body: "%{name} commented on your discovery: %{body}"! follow: "%{name} is now following you!"! mention: "%{name} mentioned you!"! splash:! want: "Want an invite?"! have: "Have an invite?"! login: "Login"! empty:! activity: "No activity yet!"! likes: "No likes yet!"! comments: "No comments yet!”! camera:! initializing: "Initializing camera..."! errors:! simulator: "Cannot capture in simulator”! crop:! initializing: "Loading image..."!
  22. Localized Strings config/locales/en.yml en:! events:! like: "%{name} liked your discovery!"!

    comment: "%{name} commented on your discovery!"! comment_body: "%{name} commented on your discovery: %{body}"! follow: "%{name} is now following you!"! mention: "%{name} mentioned you!"! splash:! want: "Want an invite?"! have: "Have an invite?"! login: "Login"! empty:! activity: "No activity yet!"! likes: "No likes yet!"! comments: "No comments yet!”! invites:! description: "You can invite up to 10 of your friends!"! camera:! initializing: "Initializing camera..."! errors:! simulator: "Cannot capture in simulator”! crop:! initializing: "Loading image..."!
  23. Localized Strings resources/en.lproj/Localizable.strings "events.like" = "%{name} liked your discovery!";! "events.comment"

    = "%{name} commented on your discovery!";! "events.comment_body" = "%{name} commented on your discovery: %{body}";! "events.follow" = "%{name} is now following you!";! "events.mention" = "%{name} mentioned you!";! "splash.want" = "Want an invite?";! "splash.have" = "Have an invite?";! "splash.login" = "Login";! "empty.activity" = "No activity yet!";! "empty.likes" = "No likes yet!";! "empty.comments" = "No comments yet!”;! "invites.description" = "You can invite up to 10 of your friends!";! "camera.initializing" = "Initializing camera...";! "camera.errors.simulator" = "Cannot capture in simulator";! "crop.initializing" = "Loading image...";!
  24. Navigation A RubyMotion UIViewController -> URL router Keep all of

    your routes tidy with Clay Allsopp’s gem, Routable
  25. Navigation class AppDelegate! def application(application, didFinishLaunchingWithOptions:launchOptions)! @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)! @window.makeKeyAndVisible!

    ! map_urls! ...! end! ! def map_urls! @router = Routable::Router.router! @router.navigation_controller = UINavigationController.alloc.init! ! @router.map("login", LoginController, modal: true)! ! @router.map("menu", MenuController, shared: true)! @router.map("profile/:id", ProfileController)! ...! @window.rootViewController = @router.navigation_controller! end! end!
  26. class AppDelegate! def application(application, didFinishLaunchingWithOptions:launchOptions)! @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)! @window.makeKeyAndVisible! !

    map_urls! ...! end! ! def map_urls! @router = Routable::Router.router! @router.navigation_controller = UINavigationController.alloc.init! ! @router.map("login", LoginController, modal: true)! ! @router.map("menu", MenuController, shared: true)! @router.map("profile/:id", ProfileController)! ...! @window.rootViewController = @router.navigation_controller! end! end! Navigation
  27. class AppDelegate! def application(application, didFinishLaunchingWithOptions:launchOptions)! @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)! @window.makeKeyAndVisible! !

    map_urls! ...! end! ! def map_urls! @router = Routable::Router.router! @router.navigation_controller = UINavigationController.alloc.init! ! @router.map("login", LoginController, modal: true)! ! @router.map("menu", MenuController, shared: true)! @router.map("profile/:id", ProfileController)! ...! @window.rootViewController = @router.navigation_controller! end! end! Navigation
  28. class AppDelegate! def application(application, didFinishLaunchingWithOptions:launchOptions)! @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)! @window.makeKeyAndVisible! !

    map_urls! ...! end! ! def map_urls! @router = Routable::Router.router! @router.navigation_controller = UINavigationController.alloc.init! ! @router.map("login", LoginController, modal: true)! ! @router.map("menu", MenuController, shared: true)! @router.map("profile/:id", ProfileController)! ...! @window.rootViewController = @router.navigation_controller! end! end! Navigation
  29. class AppDelegate! def application(application, didFinishLaunchingWithOptions:launchOptions)! @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)! @window.makeKeyAndVisible! !

    map_urls! ...! end! ! def map_urls! @router = Routable::Router.router! @router.navigation_controller = UINavigationController.alloc.init! ! @router.map("login", LoginController, modal: true)! ! @router.map("menu", MenuController, shared: true)! @router.map("profile/:id", ProfileController)! ...! @window.rootViewController = @router.navigation_controller! end! end! Navigation
  30. class AppDelegate! def application(application, didFinishLaunchingWithOptions:launchOptions)! @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)! @window.makeKeyAndVisible! !

    map_urls! ...! end! ! def map_urls! @router = Routable::Router.router! @router.navigation_controller = UINavigationController.alloc.init! ! @router.map("login", LoginController, modal: true)! ! @router.map("menu", MenuController, shared: true)! @router.map("profile/:id", ProfileController)! ...! @window.rootViewController = @router.navigation_controller! end! end! Navigation
  31. Navigation def terms_button! @terms_button ||= UIButton.new.tap do |button|! button.setTitle('Terms of

    service', forControlState: UIControlStateNormal)! ! button.addTarget(self, action: 'go_to_terms:', forControlEvents: UIControlEventTouchUpInside)! end! end! ! def go_to_terms(sender)! App.delegate.router.open(‘terms’)! end!
  32. app / routes.rb class LocalRoutes! attr_reader :router! ! def initialize(router)!

    @router = router! end! ! def map_urls! router.map('login', LoginController, shared: true)! router.map('logout') { UserAuthenticator.shared.logout }! ! router.map('feedback') do! Intercom.showNewMessageComposerWithTitleColor(! Dscovr::Colors['color_text_dark'],! barColor: Dscovr::Colors['color_background_paper'],! keyboardAppearance: UIKeyboardAppearanceLight,! success: intercom_success,! failure: intercom_failure! )! end! ! router.map('splash', SplashController, resets: true, shared: true)! router.map('explore', ExploreController, resets: true, shared: true)! router.map('explore', ExploreController, resets: true, shared: true)! router.map('feed', FeedController, resets: true, shared: true)! router.map('activity', ActivityController, resets: true, shared: true)! router.map('saved', SavedController, resets: true, shared: true)!
  33. app / routes.rb router.map('discoveries/:discovery_id', DiscoveryController)! router.map('discoveries/:discovery_id/comments', CommentsController)! router.map('discoveries/:discovery_id/likes', LikesController)! router.map('discoveries/new',

    NewDiscoveryController)! ! router.map('scout/reset', ResetPasswordController)! router.map('scout/email', ChangeEmailController)! router.map('scout/password', ChangePasswordController)! router.map('scouts/:scout_id', ProfileController)! router.map('scouts/:scout_id/followers', ScoutFollowersController)! router.map('scouts/:scout_id/following', ScoutFollowingController)! router.map('scouts/:scout_id/discoveries', ScoutDiscoveriesController)! end! ! def intercom_success! -> (response) do! p response, response.class, response.description! ! Dscovr::Blitz.success(I18n.t('intercom.success'))! ! Intercom.closeNewMessageComposer! end! end! ! def intercom_failure! -> (error) do! Dscovr::Blitz.error(I18n.t('intercom.failure'))! ! Dscovr::Log.error(error.localizedDescription)! end! end! end!
  34. app / routes.rb Dscovr::Application.routes do |r|! r.map('login', controller: LoginController, resets:

    true, shared: true)! r.map('feed', controller: FeedController, resets: true, shared: true)! r.map('settings', controller: SettingsController)! r.map('users/:id', controller: UserController)! r.map('users/:id/posts', controller: UserPostsController)! r.map('users/:id/followers', controller: UserFollowersController)! r.map('users/:id/following', controller: UserFollowingController)! r.map('posts/new', controller: NewPostController)! r.map('posts/:id', controller: PostController)! ! r.map('logout') { UserAuthenticator.shared.logout }! end!
  35. app/routes.rb module Dscovr! module Application! def self.routes! @@instance ||= Routes.new!

    ! yield @@instance! end! ! class Routes! def map(key, options = {}, &block)! router.map(key, options[:controller], options, &block)! end! ! def router! @router ||= App.delegate.router! end! end! end! end!
  36. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  37. Providing a fake library class DummyPhotoLibraryDataSource! def collectionView(collection_view, numberOfItemsInSection: section)!

    assets.count! end! ! def collectionView(collection_view, cellForItemAtIndexPath: index_path)! collection_view.dequeueReusableCellWithReuseIdentifier(PhotoGridCell::IDENTIFIER, forIndexPath: index_path).tap do |cell|! asset = assets[index_path.row]! ! cell.reset_with_asset(asset)! end! end! ! def assets! @assets ||= asset_names.map { |name| UIImage.imageNamed("images/#{name}"} }! end! ! def asset_names! %w[dummy/photo1 dummy/photo2 dummy/photo3]! end! end!
  38. Provide dummy GPS data class GPSLocator! class << self! def

    enabled?! BW::Location.enabled?! end! ! def get_location(&block)! if Device.simulator?! block.call(DummyLocation.new)! else! BW::Location.get_once do |location_or_error|! if location_or_error.is_a? Hash! Dscovr::Log.error("Unable to determine location: #{location_or_error[:error]}")! ! block.call(nil)! else! block.call(location_or_error)! end! end! end! end! end! ! class DummyLocation!
  39. Provide dummy GPS data block.call(DummyLocation.new)! else! BW::Location.get_once do |location_or_error|! if

    location_or_error.is_a? Hash! Dscovr::Log.error("Unable to determine location: #{location_or_error[:error]}")! ! block.call(nil)! else! block.call(location_or_error)! end! end! end! end! end! ! class DummyLocation! def coordinate! CLLocationCoordinate2DMake(latitude, longitude)! end! ! def latitude! 41.8907470703125! end! ! def longitude! -87.6315002441406! end! end! end!
  40. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  41. motion-layout def viewDidLoad! super! ! self.view.backgroundColor = UIColor.whiteColor! ! @label

    = UILabel.alloc.initWithFrame(CGRectZero)! @label.text = "Colors"! @label.sizeToFit! @label.center =! [self.view.frame.size.width / 2,! self.view.frame.size.height / 2]! @label.autoresizingMask =! UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin! self.view.addSubview(@label)! self.title = "Colors"! ! ["red", "green", "blue"].each_with_index do |color_text, index|! color = UIColor.send("#{color_text}Color")! button_width = 80! ! button = UIButton.buttonWithType(UIButtonTypeRoundedRect)! button.setTitle(color_text, forState:UIControlStateNormal)! button.setTitleColor(color, forState:UIControlStateNormal)! button.sizeToFit! button.frame = [! [30 + index*(button_width + 10),!
  42. motion-layout ! @label = UILabel.alloc.initWithFrame(CGRectZero)! @label.text = "Colors"! @label.sizeToFit! @label.center

    =! [self.view.frame.size.width / 2,! self.view.frame.size.height / 2]! @label.autoresizingMask =! UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin! self.view.addSubview(@label)! self.title = "Colors"! ! ["red", "green", "blue"].each_with_index do |color_text, index|! color = UIColor.send("#{color_text}Color")! button_width = 80! ! button = UIButton.buttonWithType(UIButtonTypeRoundedRect)! button.setTitle(color_text, forState:UIControlStateNormal)! button.setTitleColor(color, forState:UIControlStateNormal)! button.sizeToFit! button.frame = [! [30 + index*(button_width + 10),! @label.frame.origin.y + button.frame.size.height + 30],! [80, button.frame.size.height]! ]! button.autoresizingMask =! UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin! button.addTarget(self,! action:"tap_#{color_text}",! forControlEvents:UIControlEventTouchUpInside)! self.view.addSubview(button)! end! end!
  43. motion-layout class ColorsController < UIViewController! def viewDidLoad! super! ! self.title

    = 'Colors'! view.backgroundColor = UIColor.whiteColor! ! setup_constraints! end! ! def setup_constraints! Motion::Layout.new do |layout|! layout.view view! layout.subviews subviews_dict! layout.horizontal '|-[red]-[green(==red)]-[blue(==red)]-|'! layout.vertical '[label]-30-[green]'! end! ! [NSLayoutAttributeCenterY, NSLayoutAttributeCenterX].each do |attribute|! view.addConstraint(NSLayoutConstraint.constraintWithItem(! label,! attribute: attribute,! relatedBy: NSLayoutRelationEqual,! toItem: view,! attribute: attribute,! multiplier: 1.0,! constant: 0.0))!
  44. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  45. module Dscovr; module Colors! class << self! def [](name)! colors[name].to_color!

    end! ! def colors! { 'dark_color' => '#232323',! 'background_color' => '#ecf0f1',! 'highlight_color' => '#eb5d4e',! 'color_background_paper' => '#ffffff',! 'color_background_ghost' => '#f5f7f9',! 'color_background_soft' => '#c5c7c9',! 'color_background_light' => '#eaf0f1',! 'color_background_medium' => '#8c959b',! 'color_background_medium_dark' => '#525253',! 'color_background_dark' => '#394349',! 'color_text_inverted' => '#ffffff',! 'color_text_light' => '#7f8c8c',! 'color_text_medium' => '#bdc3c7',! 'color_text_medium_light' => '#999999',! 'color_text_medium_dark' => '#8c959b',! 'color_text_dark' => '#394349',! colors # BubbleWrap::String#to_color lib/dscovr/colors.rb
  46. fonts lib/dscovr/fonts.rb module Dscovr; module Fonts! class << self! def

    [](request)! font, size = request.split(',')! ! UIFont.fontWithName(fonts[font], size: size.to_f)! end! ! private! ! def fonts! { 'light' => 'HelveticaNeue-Light',! 'normal' => 'HelveticaNeue',! 'bold' => 'HelveticaNeue-Bold',! 'icon' => 'dscovr' }! end! end! end; end!
  47. DB5 - (void)viewDidLoad {! self.view.backgroundColor = [self.theme colorForKey:@"backgroundColor"];! self.label.textColor =

    [self.theme colorForKey:@"labelTextColor"];! self.label.font = [self.theme fontForKey:@"labelFont"];! ! [self.theme animateWithAnimationSpecifierKey:@"labelAnimation" animations:^{! CGRect rLabel = self.label.frame;! rLabel.origin = [self.theme pointForKey:@"label"];! ! self.label.frame = rLabel;! ! } completion:^(BOOL finished) {! NSLog(@"Ran an animation.");! }];! }!
  48. DB5 <?xml version="1.0" encoding="UTF-8"?>! <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"

    "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">! <plist version="1.0">! <dict>! ! <key>Default</key>! ! <dict>! ! ! <key>backgroundColor</key>! ! ! <string>708090</string>! ! ! <key>labelTextColor</key>! ! ! <string>ececec</string>! ! ! <key>labelFont</key>! ! ! <string>Avenir-Medium</string>! ! ! <key>labelFontSize</key>! ! ! <integer>18</integer>! ! ! <key>labelX</key>! ! ! <integer>44</integer>! ! ! <key>labelY</key>! ! ! <integer>300</integer>! ! ! <key>labelAnimationDuration</key>! ! ! <real>0.25</real>! ! ! <key>labelAnimationDelay</key>! ! ! <real>2</real>! ! ! <key>labelAnimationCurve</key>! ! ! <string>easeout</string>!
  49. • Automate your workflow • Organizing your code • Be

    simulator friendly • Use auto layout • Make it easy for designers to contribute • CI with Travis
  50. RubyMotion on Travis CI ---! language: objective-c! osx_image: mavericks! rvm:!

    - 2.1.1! before_install:! - bundle install && bundle exec rake pod:install! script: bundle exec rake spec! env:! global:! - COCOAPODS_NO_REPO_UPDATE_OUTPUT=1!
  51. RubyMotion on Travis CI https://gist.github.com/johanneswuerbach/5559514 ---! language: objective-c! before_script:! -

    ./scripts/travis/add-key.sh! after_script:! - ./scripts/travis/remove-key.sh! after_success:! - ./scripts/travis/testflight.sh! env:! global:! - APPNAME="NAME_OF_THE_APP"! - 'DEVELOPER_NAME="iPhone Distribution: NAME_OF_THE_DEVELOPER (CODE)"'! - PROFILE_UUID=PROVISIONING_PROFILE_UUID!