News

 
  • 21 May

    MotionBundler: Good Old Fashioned Requirements for RubyMotion

    (This is a guest post from Paul Engel, creator and maintainer of MotionBundler)

    When I started writing my first RubyMotion application, I almost immediately wanted to use a Ruby gem to accomplish a certain goal. After setting up my Gemfile, running bundle and rake in the Terminal, I soon discovered that it wasn’t possible to just require any random gem I wanted to: it had to be aware of it being required in a RubyMotion app.

    Suddenly, my next open source project became a fact. I wanted to create a gem which allowed you to require any Ruby gem in a RubyMotion application. My first attempt was the LockOMotion project. It gets the job done but it wasn’t test-driven enough and the code wasn’t well structured. Enter MotionBundler, a complete rewrite with a 100% test-coverage.

    I am a big fan of using and writing libraries and gems that are as unobtrusive as possible. I did not want to force the developer to follow special conventions (e.g. using another method than require to load up code). And MotionBundler does just that.

    Setup and Usage

    MotionBundler is intended to be easy in installation and usage. You need to setup your Gemfile by separating RubyMotion-aware Ruby gems from the ones that are not. Put the RubyMotion-unaware gems in the :motion Bundler group, as shown here.

    source "http://rubygems.org"
    
    # RubyMotion aware gems
    gem "motion-bundler"
    
    # RubyMotion unaware gems
    group :motion do
      gem "slot_machine"
    end
    
    Then, simply add MotionBundler.setup at the end of your Rakefile.
    [...]
    require "motion/project"
    
    # Require and prepare Bundler
    require "bundler"
    Bundler.require
    
    Motion::Project::App.setup do |app|
      # Use `rake config' to see complete project settings.
      app.name = "SampleApp"
    end
    
    # Track and specify files and their mutual dependencies within the :motion 
    # Bundler group
    MotionBundler.setup
    

    Finally, you can just run the bundle command then the default rake task to build and run the application. And that’s all about it!

    Requiring non-Gem Sources

    Just like a regular Ruby project, you are now able to require source files. It can either be a relative path on the file system or a Ruby standard library source file. As an example, let’s require the cgi Ruby standard library file and use the CGI.escape_html method in a RubyMotion controller.

    require "cgi"
    
    class AppController < UIViewController
      def viewDidLoad
        puts CGI.escape_html('foo "bar" ')
      end
    end
    

    Looks familiar, right?

    How Does MotionBundler Work?

    MotionBundler traces require, require_relative, load and autoload method calls in your code when invoking MotionBundler.setup. All four methods eventually are delegated to the require method which MotionBundler hooks into. MotionBundler calls MotionBundler::Require::Tracer::Log#register which traces the calling Ruby source and registers the source file being loaded.

    When MotionBundler.setup invokes Bundler.require :motion, all required files and their mutual dependencies are registered. Aside from the Ruby gems defined in the :motion Bundler group, MotionBundler also uses Ripper::SexpBuilder to scan for require statements (like motion-require does) in the Ruby sources defined in ./app/**/*.rb so it can trace requirements using MotionBundler::Require::Tracer::Log.

    Console Warnings

    When I was writing LockOMotion it was very difficult to debug certain errors. I have been able to override certain core methods to print warnings which contain useful debug information.

    These are only printed in the iOS simulator console. When running from the device, MotionBundler does not interfere when an error gets raised. As an example, let’s check out a warning printed in the console when dealing with a runtime require statement.

    [...]
       Warning Called `require "base64"`
               Add within setup block: app.require "base64"
    2013-05-21 13:45:26.851 SampleApp[17300:c07] app_controller.rb:48:in `viewDidLoad': uninitialized constant AppController::Base64 (NameError)
        from app_delegate.rb:5:in `application:didFinishLaunchingWithOptions:'
    2013-05-21 13:45:26.855 SampleApp[17300:c07] *** Terminating app due to uncaught exception 'NameError', reason: 'app_controller.rb:48:in `viewDidLoad': uninitialized constant AppController::Base64 (NameError)
        from app_delegate.rb:5:in `application:didFinishLaunchingWithOptions:'
    [...]
    

    Here, the base64 file is missing from the build system. You can fix that problem by adding the following code in the Rakefile.

    MotionBundler.setup do |app|
      app.require "base64"
    end
    

    RubyMotion Runtime Limitations

    Unfortunately, you still cannot just require every file that works within a regular Ruby environment. You cannot require C extensions and you cannot evaluate Ruby code passed in a String (e.g. the eval method). This is why MotionBundler cannot ensure that you can require every Ruby gem out there. They have to be RubyMotion friendly, for example like SlotMachine.

    But as a last resort, MotionBundler offers you to possibility to mock source requirements by loading drop-in replacements written in pure Ruby.

    Mocking Sources

    Let’s say you want to use a Ruby gem which requires the stringio extension. Because stringio is a Ruby C extension, and that RubyMotion doesn’t support Ruby C extensions, that Ruby gem will not load up in RubyMotion. However, as mentioned earlier, MotionBundler offers a possibility to bypass this problem by mocking the library.

    Instead of requiring the stringio.bundle file, MotionBundler is able to mock it with a pure Ruby implementation of it, taken from MacRuby. At the moment, MotionBundler also mocks strscan, zlib and httparty (only its basic features).

    Aside from mocks being defined within the MotionBundler gem, you can also define your own mocks within your RubyMotion application. Just add a directory called mocks within the root directory of the application and put the mock sources in it. The relative path of the mock source within that directory ensures a certain Ruby file being mocked during compile time.

    Let’s say the root directory of your RubyMotion application is ~/Sources/sample_app. If you want to mock require "yaml", create a file at ~/Sources/sample_app/mocks/yaml.rb containing the mock code. If you want to mock require "net/http", create a file at ~/Sources/sample_app/mocks/net/http.rb.

    You aren’t supposed to mock entire Ruby gems of course, that would be crazy. But you would rather mock fundamental Ruby standard library sources (like stringio.bundle) so you don’t have to dismiss a certain Ruby gem for use in your RubyMotion app on forehand.

    Please Try MotionBundler!

    MotionBundler is available on Github. The repository provides a sample application. You can clone the repository, navigate to the sample application directory, run bundle followed by rake. Please check out MotionBundler! Pull requests, remarks or requests are very welcome! You can also contact me on Twitter :-)

    Finally, I would like to say thanks to Laurent, Nick Quaranto and Dave Lee for giving me some feedback and suggestions.

    You can discuss this post on the RubyMotion google group.

    Paul Engel is a software developer living in beautiful Amsterdam, Netherlands.

 
 

Want to stay in touch?

Follow us on Twitter