Developing iOS applications with Ruby

Mario ChavezJan 16, 2013

Lately, RubyMotion has become an increasingly popular tool for developing iOS applications with Ruby. When one has a hands-on experience with the interface is easy to tell why Ruby is a much more appealing language than Objetive-C.

Introduction

Is it Ruby or not?

RubyMotion is a toolchain for developing iOS application using the Ruby language. It consists of a Runtime which implements a piece of Ruby code customized for iOS. Although this type of Ruby code has a different use than CRuby, it is based on the Ruby 1.9 specifications.

Knowing Ruby will not necessarily mean that you can write iOS applications with RubyMotion, but it will definitively be of help. Nonetheless, it is significantly more important to be familiar with Objetive-C and the Foundation Framework API to work with it. As might be, Objetive-C has been "Rubyfied" with RubyMotion.

Project management

To work with RubyMotion you don't necessarily need XCode, you can use your own favorite editor. RubyMotion comes with a command that helps you to create a project directory and set up basic files for you.

It also has a set of rake tasks that helps you build your project and start your application on an iOS simulator. It will recognize resources like images or pfiles, as well as include and use .xib, .storyboard and .xcdatamodeld files.

How do I start with RubyMotion?

If you want to get started with RubyMotion first you need buy a license for it, since RubyMotion is proprietary software. After that, it would be a good idea to take a look at the Getting Started guide on the RubyMotion website.

Also useful is the introductory screencast from Motion Casts. And a 50-minute screencast on how to build a simple application is available from Pragmatic Studio. This tutorial is also a good starting point.

Sample application

Now that we have an idea on what is RubyMotion and what it can do for us, let's work on a sample application.

Although I couldn't find a more complex code example, here I present one that I wrote myself. A sample source code is available with this post.

Our application

We will pretend that we are building an application for a conference, for this I will use the schedule for the MagmaRails 2012 conference. We are going to reduce the visible information to just the speakers and their conferences organized by day.

Our user interface

On the RubyMotion developer's site all resources point to building the user interface with code, there are even some DSLs explicitly created to build iOS user interfaces. But instead of going for that, we are going to use XCode Storyboard to create our application's User Interface and help us model our navigation path.

Storyboards are tied to XCode, and I have said before that we can develop RubyMotion apps without the need of XCode, but in this case we are going to use it to build our user interface, simply because Storyboards can very helpful when visualizing a complex navigation path.

Preparing our project

As a first step, we are going to create an XCode new project - I assume you are using XCode 4.5.x - and have selected a Master-Detail Application, so give it a name and be sure that on Devices, iPhone is selected and also that options Use StoryBoard and Use Core Data are checked.

image alt

Now let's create our RubyMotion project, let's name it "conference"

$ motion create conference

Inside our RubyMotion project structure we will have a resources directory, there we are going to copy the MainStoryboard.storyboard file that was created by XCode, right-click on the file and select Show in Finder, locate the file and move it to the resources directory in our RubyMotion project and then delete the file reference in XCode.

image alt

Since we still want to be able to modify this file from XCode let's drag and drop the same file from our resources directory into the XCode interface, when doing so we will be asked for options to add the file back to XCode, verifiy that Copy items into destination group's folder (if needed) is unchecked and click on Finish. This action will create a link to that file, so in this case, every time we modify it from XCode we are modifying the copy that we have at our resources directory.

image alt

Now let's set up Bundler in our RubyMotion project, let's execute:

$ bundle init

Modify Gemfile file to make it look like:

source :rubygems

gem 'xcodeproj', '~> 0.3.0'
gem 'ib'
gem 'rake'

Save the file and run Bundler

$ bundle

Gems xcodeproj and ib will help us to connect outlets from our Ruby code with the XCode storyboard, we will use them a little later in our development.

Let's edit our Rakefile and after the "require "motion/project"" line add the following piece of code that will enable Bundler on our project:

require 'rubygems'
require 'ib'
require 'bundler'

 Bundler.require

Find the app_delegate.rb file and open it with your editor, inside we have a method application defined, which is the starting point for our application, here we are going to tell RubyMotion to load our MainStoryboard.storyboard file, just replace application method with these lines:

def application(application, didFinishLaunchingWithOptions:launchOptions)
  @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)

  @storyboard ||= UIStoryboard.storyboardWithName('MainStoryboard', bundle:NSBundle.mainBundle)
  @window.rootViewController = @storyboard.instantiateInitialViewController

  @window.rootViewController.wantsFullScreenLayout = true
  @window.makeKeyAndVisible

  true
end

We initialize a frame for in our screen, load the storyboard, then set rootViewController from the storyboard main controller and make our screen visible.

To test it, just run:

$ rake

This will compile our RubyMotion project into an iOS application, load the simulator and load and run our application, if everything went OK you should see an empty grid. Type quit in the console to close our application.

image alt

Create our Storyboard

Going back to XCode, let's open the MainStoryboard.storyboard file.

image alt

Select the Master View Controller and double click on the title to change it to MagmaRails. Now select the Table View and change Content property from Dynamic to Static, the table will show three cell rows now, select the first one and duplicate it twice. Change the title for each cell - with a double click - to "Day One", "Day Two", "Day Three", "Speakers" and "Venue". Click on the segue that connects Master View Controller with Detail View Controller to select and delete it.

image alt

We need another View Controller to display the conferences by day, so let's drag it from the Objects Library into our canvas. Drag a table view into this new controller and drag a table-view cell into the table view.

Using the identity inspector rename our controller to TalksViewController. For our new table-view cell in the attributes inspector change its identifier to Talk and from identity inspector change cell class to TalkViewCell, also be sure that our cell style is custom and change cell height in size inspector to 115.

Add 3 labels and one image to the cell to represent the information that we want to display there. Once you are done, press ctrl and drag from cell Day one on Master View Controller into the new Talks View Controller to create a segue. Select push as the type of the segue and assign 'DayOne' as it's name. Do the same for cells Day two - 'DayTwo' - and Day Three - 'DayThree' -.

image alt

Core Data models and application seed

Before we move on, let's create our Code Data models and the seed data that we are going to use to display in our application.

When we created our project with XCode, the Use Core Data option was checked, this means that a .xcdatamodeld was created with our project, find that file in XCode and right click on it, select Show File in Finder, move the file to our resources directory in our RubyMotion application and delete the file reference on XCode, now drag the file from our resources directory to XCode to put it back in our XCode project.

In XCode open our XCode Core Data Models file and add two entities, Talk and Presenter. For Talk add the attributes as shown in the image, and also be sure that Class is set to Talk in the Data Model Inspector. Do the same for Presenter.

image alt

image alt

Save the XCode Core Data Models file. Now we are ready to create our Models in our project in RubyMotion.

Here we are going to require our Gemfile two gems. First we are going to use motion-cocoapods, which allow us to summon our project static libraries packaged in pods. Motion-cocoapods is a wrapper that enables Rubymotion to use cocoapods, which is a dependency manager for Objective-C; this means that even when we are coding our application with Ruby, we will be able to use existing Objective-C libraries.

The library that we are going to use is called MagicalRecord and will help us work with Core Data effortlessly.

The second gem is motion_support which gives us Inflector methods and some core extensions like Ruby On Rails ActionSupport, but specific for RubyMotion. Add gems to Gemfile and execute the Bundler command.

gem 'motion-cocoapods'
gem 'motion_support'

In order to have both gems available open your Rackfile and add the following two require lines just below the require for rubygems.

require 'motion-cocoapods'
require 'motion_support/all'

Also we need to tell RubyMotion that we want to use the Core Data framework and require MagicalRecord library from cocoapods. This is a good time to instruct RubyMotion to load our app/lib files before any other file in our project, so let's also modify the app.files setting.

Inside of the block Motion::Project::App.setup, add the following lines at the end:

app.frameworks += %w(CoreData)

app.files.unshift Dir.glob(File.join(app.project_dir, 'app/lib/**/*.rb'))

app.pods do
  pod 'MagicalRecord'
end

After these changes, set up motion-cocoapods, from the console, type:

$ pod setup
$ rake UPDATE=1

These two lines will set up cocoapods and install the Objetive-C libraries defined in our Rakefile.

After the completions of the last steps we should be ready to use Core Data in our application.

Now we need to create a new class for each model that we described in our Core Data diagram. So if we don't already have a model directory inside the app directory let's create one and add two classes: Talk.rb and Presenter.rb

class Talk < NSManagedObject
end

class Presenter < NSManagedObject
end

We need to define some methods to make our models act as Code Data entities. It's important to note that we don't need to define any properties to hold our data and relationships in our models. These methods are defined for us via the definition in our Core Data diagram.

Our models will require methods to inflate a model from a hash arrangement and a couple of finders to filter data, here is were MagicalRecord will come in handy.

Also in our app_delegate.rb file we will need a method to seed our database the first time, this method will be called when our application starts. Seed data is stored in a plist file, which is transformed into a hash and then used to inflate and save our models to our local database.

def seedDatabase
 MagicalRecord.setupCoreDataStackWithStoreNamed('database.sqlite')

  if Talk.allTalks.size == 0
    #https://github.com/Bodacious/PListReadWrite
    PListRW.copyPlistFileFromBundle(:seed)
    seed = PListRW.plistObject(:seed, Hash)

    presenters = []
    seed['presenters'].each do |presenter_attrs|
      presenters << Presenter.createWithHash(presenter_attrs)
    end

    seed['talks'].each do |talk_attrs|
      talk = Talk.createWithHash(talk_attrs)
      presenter = presenters.select{|p| p.presenterId == talk.presenterId }.first

      talk.presenter = presenter
      talk.save

      presenter.addTalk(talk)
      presenter.save
    end
  end
end

Working with our controllers

Back when we worked with xCode in our Storyboard we had created segues that connected our MasterViewController with TalksViewController, we created 3 segues, one for each conference day.

Since all 3 segues points to the same controller we need to use the segue identifier to let TalksViewController now for what day do we want to see the list of talks.

Create a new file in app/controllers called masterviewcontroller.rb and add a method prepareForSegue:

class MasterViewController < UITableViewController

  def prepareForSegue(segue, sender: sender)
    case segue.identifier
    when 'DayOne', 'DayTwo', 'DayThree'
      segue.destinationViewController.setFilter segue.identifier
    end
  end

end

The destinationViewController property refers to the destination controller, in our case TalksViewController, and we assume that it should have a propety filter that will receive the criteria to filter our talks.

So let's create our controller TalksViewController. As we said earlier, we need a property filter for it and since this controller is going to be the delegator for a TableUIView we need to implement two more methods:

class TalksViewController < UIViewController
  attr_accessor :filter
  attr_accessor :dataSource

  def tableView(tv, numberOfRowsInSection:section)
    self.dataSource.count
  end

  def tableView(tv, cellForRowAtIndexPath:indexPath)
    @reuseIdentifier ||= 'TalkCell'

    cell = tv.dequeueReusableCellWithIdentifier(@reuseIdentifier) || begin
    TalkCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier)
    end

    talk = self.dataSource[indexPath.row]

    # We will come back to this a bit later
   end

   def viewDidLoad
     day = case filter
       when 'DayOne' then 1
       when 'DayTwo' then 2
       when 'DayThree' then 3
       end

     self.dataSource = Talk.talksByDay(day)
  end
end

In our viewDidLoad method we check for the filter value that we passed on the prepareSegue method and then we filter out the talks data base on it and store it in a property dataSource.

Our method tableView(tv, numberOfRowsInSection:section) needs to know how many rows we are going to display in our table, here we just query our datasource for this information.

tableView(tv, cellForRowAtIndexPath:indexPath) is a bit more tricky, this method is called every time a row is displayed in our screen, here we are required to provide the cell object that is going to be displayed with all the proper data. If our data source has tenths or hundreds records, this could lead to a bad memory drainage. What the framework does is to keep a pool of previously instanced cells and, instead of creating a new cell, it's taken from this pool and reused, this keeps the number of memory objects low. This method reuses cells using an identifier and if no available cell is found then a new one is created.

In the case of our Storyboard we did a custom cell for talk details, and also added TalkCell as cell identifier, which has a few labels and an image. So, let's create a new class TalkViewCell at file /app/cells/talkviewcell.rb:

class TalkViewCell < UITableViewCell
end

Next we need to add IB Outlets which are properties that will allow us to connect our code with labels and images in our Storyboard, here we are going to get some help from a Gem that we added at setup time, ib, so after adding outlets our class looks like:

class TalkViewCell < UITableViewCell
  extend IB

  outlet :talk, UILabel
  outlet :speaker, UILabel
  outlet :day, UILabel
  outlet :picture, UIImageView
end

Once that we have our outlets in place, let's connect them in our Storyboard. If you have XCode open, please close it and execute the following command - provided by gem ib -:

$ rake ib:open

This will open a fake ib XCode project, which can be safely be closed, open your Storyboard with XCode and select the TalkViewCell from it and click on the connections inspector, there you should see the outlets that we defined in our class, they should be connected now.

Drag from the circle to the right of the outlet name to the element in the canvas and save the Storyboard.

image alt

Open the TalkViewCell class again and add the following method, which will be used to set cell data:

def setupTalk(talk)
  self.talk.text = talk.title
  self.speaker.text = talk.presenter.name
  self.picture.image = UIImage.imageNamed(talk.presenter.picture)
  self.day.text = "Day #{talk.day}, #{talk.time}"
end

Now go back to the TalksViewController to the bottom of method tableView(tv, cellForRowAtIndexPath:indexPath) and replace the comment "# We will come back to this a bit later" with:

cell.setupTalk(talk)

cell

Execute your project in the simulator with:

$ rake

Your should get the following screenshot.

image alt

Press on Day Two and you should get the list of talks for that day.

image alt

Final thoughts

If you got to this point, you were able to build a RubyMotion application for an iPhone that uses:

  • Bundler and Gems specific to RubyMotion
  • Cocoapods and Objetive-C libraries in the mix
  • XCode Core Data diagrams and sqlite local database
  • the XCode interface builder and a way to connect it to our Ruby code

If you want to experiment with the code from this post visit Conference and check out the project.

RubyMotion makes working with iOS application a joy.

Additional References

blog comments powered byDisqus