Everyone is trying to craft the next beautiful iOS app, but building on Apple’s platform has traditionally required experience in a niche programming language, Objective-C. However, with the release of RubyMotion, anyone can make a completely native iOS app using the power of Ruby.
Developers have tried to get around the Objective-C hurdle by making HTML and JavaScript hybrid apps using tools like PhoneGap and Trigger, but the result can be a substandard user experience. Plus, mobile-centric Web development is yet another narrow skill set that potential developers have to learn. RubyMotion is intended to be an alternative solution that produces apps identical to those created in Objective-C, except using a more accessible and popular language.
Further Reading on SmashingMag:
- How To Get Started With Ruby
- Get Started Writing iOS Apps With RubyMotion
- A Guide To Starting Your Own Rails Engine Gem
- How To Set Up An Ubuntu Local Machine For Ruby On Rails
How does that work? Unlike normal interpreted Ruby, RubyMotion is compiled to machine code and runs incredibly fast. This allows full access to the existing iOS SDK and APIs while preserving the flexibility and plain fun of Ruby. RubyMotion also includes interactive debugging and testing tools that don’t exist in the traditional Xcode and Objective-C workflow.
The hope is that coding in Ruby and using the RubyMotion toolchain will make developing iOS apps easier for new developers, as well as help existing iOS developers be even more productive. I’ve been working on iOS apps since the original SDK, but I still jumped at the chance to write real native apps in the same language that I was already using for my back ends.
If you’ve hit stumbling blocks learning native iOS development or are just curious about what Ruby on iOS looks like, you should read on. We’ll try out RubyMotion by making an app that grabs some data from the Internet and updates the screen’s content accordingly.
But first, we have to install it.
Set Up
RubyMotion is a commercial product from HipByte, a company founded by the developers responsible for MacRuby. Parts of the RubyMotion tools are open source, but the actual Ruby compiler (where the magic happens) is not. Even though it’s only a few months old, a strong community has already developed around RubyMotion; I’m involved in maintaining some cool projects but am in no way affiliated with HipByte.
You can purchase a lifetime license to RubyMotion for $200 on the RubyMotion website. The license comes with a graphical installer that sets up everything on your Mac, no commands necessary. If you run into trouble, support is available on @RubyMotion’s Twitter account and the mailing list.
RubyMotion also requires you to install Xcode from the Mac App Store to get some developer libraries and tools. However, RubyMotion’s tools work in the command shell, and you’re free to use any editor or IDE you want. Supplementary packages are available for most editors that could speed up your development.
Having some experience with RubyGems and Rake also helps because RubyMotion uses these tools. RubyGems should come installed on your Mac, and you can use it to install Rake by running gem install rake
in the terminal. If you have never seen those words before, no worries; we’ll still cover them like fresh topics.
Create A Project
In contrast to the normal iOS toolchain, RubyMotion works in the command line. You can still use Xcode’s Interface Builder to construct your interfaces, but complete Xcode integration is unsupported at this time. So, fire up the terminal and let’s get started.
RubyMotion uses two commands extensively: rake
and motion
. The motion
command creates RubyMotion projects and manages the RubyMotion tools; if you’re coming from a Rails background, it’s sort of like the rails
command. If you enter motion
in the shell, you’ll see a brief instruction page:
$ motion
Usage:
motion [-h, --help]
motion [-v, --version]
motion <command> [<args…>]
Commands:
create Create a new project
activate Activate the software license
update Update the software
support Create a support ticket
We’re interested in motion create <Project Name>
. This will create the folder <Project Name>
in the current directory and fill it with the essential files and folders that you’ll need for a RubyMotion project.
Run motion create Smashing
to create your first project. You should see some output like this:
$ motion create Smashing
Create Smashing
Create Smashing/.gitignore
Create Smashing/Rakefile
Create Smashing/app
Create Smashing/app/app_delegate.rb
Create Smashing/resources
Create Smashing/spec
Create Smashing/spec/main_spec.rb
Run cd ./Smashing
to enter the project’s directory. You’ll be running all subsequent commands from this location, so keeping it open in a dedicated tab or window is a good idea.
Let’s walk through what this command created:
./Rakefile
This is the file that therake
command uses to determine what commands are available. RubyMotion also uses it for settings such as your app’s name, resources and source-code location../app
This is the directory that contains all of your code. RubyMotion will recursively dig through this folder and load any*.rb
files that it finds. You can specify additional directories outside of./app
inRakefile
../app/app_delegate.rb
This is your only piece of code right now. It contains your app’sdelegate
. We’ll go into more detail soon, but know that every RubyMotion project needs adelegate
../resources
Files in this directory will be copied into your app. It’s a good place to store images, data and icons../spec
This is the directory for your app’s automated tests. RubyMotion ships with a port of the Ruby testing framework Bacon, which you can use to write both unit and functional or UI tests. Any*.rb
files in this directory will be executed as tests when you invoke therake spec
command../spec/main_spec.rb
This is the default test, created as an example.
Compared to larger frameworks such as Rails, this isn’t a lot of configuration. The only files we really care about today are Rakefile
and app_delegate.rb
, so let’s dive into those.
Run The App
The Rakefile
is the first file loaded when you build your app. Its job is to configure your app’s properties and load any additional files that your project might need. By default, it looks something like this:
# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
Motion::Project::App.setup do |app|
# Use 'rake config' to see complete project settings.
app.name = 'Smashing'
end
You’ve probably only seen $.unshift
if you’re familiar with Ruby. It takes its argument — in this case, /Library/RubyMotion/lib
— and adds it to require
’s search path. This is necessary before require ‘motion/project’
, because that file is actually located in the RubyMotion lib
directory. Without the unshift
, no RubyMotion code would be found.
The motion/project
directory is what actually allows us to write RubyMotion apps. It does a lot of stuff behind the scenes, but most obviously it includes the Motion::Project
module that we use immediately afterwards. The App.setup
block is where we can edit our app’s name, files, identifier and many other options. As the generated comment suggests, you can run rake config
to see all possible properties. By default, it uses the Smashing
project name that we passed in motion create
.
When you run rake
, it will load the Rakefile
. Requiring motion/project
actually creates a bunch of rake
“tasks.” These tasks allow you to pass arguments to rake
to invoke particular actions. You can see a complete list of included tasks by running rake –tasks
in your project’s directory:
$ rake --tasks
rake archive # Create archives for everything
rake archive:development # Create an .ipa archive for development
rake archive:release # Create an .ipa for release (AppStore)
rake build # Build everything
rake build:device # Build the device version
rake build:simulator # Build the simulator version
rake clean # Clear build objects
rake config # Show project config
rake ctags # Generate ctags
rake default # Build the project, then run the simulator
rake device # Deploy on the device
rake simulator # Run the simulator
rake spec # Run the test/spec suite
rake static # Create a .a static library
Looks like rake
does quite a bit, doesn’t it? Most importantly, if you just run rake
, it will build and run your app in the Simulator.
Run rake
and observe RubyMotion compiling your project’s files (just app_delegate.rb
for now). When it’s done building, it will open your (so far empty) app in the iOS Simulator:
Additionally, you’ll see an irb
-esque prompt appear in the shell. This allows you to interact with the app in real time without any additional compilation, which is useful for debugging and rapid interface development. Try running some basic commands with it:
$ rake
…
(main)> "a string"
=> "a string"
(main)> h = {hello: "motion"}
=> {:hello=>"motion"}
(main)> h
=> {:hello=>"motion"}
Our app is off to a great start: we’ve installed RubyMotion, created a new project, learned what all the files do, and gotten a (very bare) app running. Next, we’ll make our creation actually display something on the screen.
Little Boxes
We’re going to build an app that displays a colored box on the screen. Sounds pretty simple, right?
Then we’ll spice it up with random color changes using the Colr API. It’s going to use a mix of Apple-developed APIs and some cutting-edge work from the RubyMotion community, so when it’s done you’ll have gotten a well-rounded experience with RubyMotion development. We will end up with something like this:
If you want to follow along, the source for this example is available on GitHub.
Open up app_delegate.rb
in your favorite code editor. It’s pretty barren, implementing only one function, application:didFinishLaunchingWithOptions:
:
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
true
end
end
Note that we refer to RubyMotion functions by a combination of their usual Ruby name (application
) and their named parameters (didFinishLaunchingWithOptions:
), all separated by colons. Named parameters were added to RubyMotion to preserve the existing Objective-C APIs, and the extra symbols are required parts of the method name (i.e. you can’t just call delegate.application(@app, options)
). Without those extra parameters, we wouldn’t be able to tell the difference between def application(application, didFinishLaunchingWithOptions:launchOptions)
and def application(application, handleOpenURL:url)
.
Moving on, RubyMotion looks for a class named AppDelegate
and makes it the application’s delegate
object. This special object receives callbacks for different events in the lifecycle of the app, such as for starting up, shutting down and receiving push notifications. The application:didFinishLaunchingWithOptions:
function is called once the system has finished its own process of starting the app and is ready for us to take control. In most cases, this function should return true
and allow everything to start.
We’re going to add some “views” to our app. Each view is a subclass of UIView
, and everything you see on the screen is a descendent of that class. The root view of every app is an instance of UIWindow
, a special type of UIView
. Views are added as subviews
to one another; when you move a view, you also move all of its subviews.
Each view has a frame
property, which describes its position and dimensions. The position of a view is actually defined relative to its superview. For example, adding a box at (10, 10)
as a subview to a view located at (20, 20)
in the window means that our new box will really appear at (30, 30)
.
Edit your AppDelegate
to include our new views:
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
# UIScreen describes the display the app is running on.
app_frame = UIScreen.mainScreen.applicationFrame
@window = UIWindow.alloc.initWithFrame(app_frame)
# This is the special method of UIWindow which lets them exist outside of a parent view
@window.makeKeyAndVisible
# This is our blue box
# CGRectMake == CGRectMake(x, y, width, height)
@box = UIView.alloc.initWithFrame CGRectMake(0, 0, 100, 100)
@box.backgroundColor = UIColor.blueColor
@window.addSubview(@box)
# UIButtonTypeRoundedRect is the standard button style on iOS
@button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
# A button has multiple "control states", like disabled, highlighted, and normal
@button.setTitle("Change Color", forState:UIControlStateNormal)
# Sizes the button to fit its title
@button.sizeToFit
# Position the button below our box.
@button.frame = CGRectMake(0, @box.frame.size.height + 20, @button.frame.size.width, @button.frame.size.height)
@window.addSubview(@button)
true
end
end
We’ve created our window and made it the “key” window, which means that it’s the window receiving user input. We then added our blue @box
and @button
as subviews to the window.
Now, this is not the only way to add views to our window. We could have added our views using the Interface Builder, a tool included with Xcode for creating iOS UIs with a drag-and-drop interface. RubyMotion does support Interface Builder, but you really shouldn’t dive into it without understanding what happens behind the scenes.
The other method is to use something called a UIViewController
and then to populate its view
property with our boxes. This is actually the correct way because it conforms your app to the model-view-controller design pattern that the SDK follows. You simply tell the window
to load the controller
, and everything will get handled appropriately. So, why didn’t we do it this way? Because it requires more information overhead, and in this example we’re trying to be as concise as possible; in production code, you will definitely need to use a controller when you start managing more than one or two views.
Moving on, rake
our improved delegate and check out the result, which is starting to look better:
Now we need to make @button
actually do something besides turn blue when we press it. Buttons can take target
s for certain events such as taps. We can add a target and callback (known as an action
in iOS SDK parlance) in our AppDelegate
like this:
…
@window.addSubview(@button)
@button.addTarget(self, action:"button_tapped", forControlEvents:UIControlEventTouchUpInside)
true
end
def button_tapped
puts "I'm tapped!"
end
end
UIControlEventTouchUpInside
is one of many UIControlEvent
s that a button can respond to. It sounds complicated, but in plain English it refers to a “touch” that lifts “up” and is still “inside” of the button’s rectangle. When that occurs, button_tapped
will be called. The puts
prints a string to the terminal when we run the app in the simulator.
Give it a rake
and confirm for yourself that our message prints out:
$ rake
Build ./build/iPhoneSimulator-5.1-Development
Compile ./app/app_delegate.rb
Link ./build/iPhoneSimulator-5.1-Development/Smashing.app/Smashing
Create ./build/iPhoneSimulator-5.1-Development/Smashing.dSYM
Simulate ./build/iPhoneSimulator-5.1-Development/Smashing.app
(main)> I'm tapped!
We’ve thrown some views onto our previously barren app and added some very basic interactivity. Time to hook it up to the good ol’ information superhighway.
HTTP
Now that we’ve got a box and a button on the screen, let’s grab some data from the Internet. To make our networking code painless, we’ll use BubbleWrap, a popular RubyMotion library filled with many idiomatic Ruby wrappers. It’ll also handle JSON de-serialization, so it’s the only external component we need.
To install BubbleWrap, run gem install bubble-wrap
. In our Rakefile, we need to require bubble-wrap
:
…
require 'motion/project'
require 'bubble-wrap'
…
This makes the BubbleWrap::HTTP
and BubbleWrap::JSON
libraries available to us. The default BubbleWrap
set-up also includes helpers for UIColor
, which we’ll use to convert a hexadecimal color code like #f8f8f8
into a UIColor
object.
Now on to the dirty work. The Colr API has an endpoint that returns information about a random color in JSON. We’re going to query that, get the hex representation of that color, and set our box’s backgroundColor
to that value. In AppDelegate
, let’s add the code to make this HTTP call in our button callback:
def button_tapped
BubbleWrap::HTTP.get("http://www.colr.org/json/color/random") do |response|
color_hex = BubbleWrap::JSON.parse(response.body.to_str)["colors"][0]["hex"]
# ensure that color_hex is a String when we run .to_color
@box.backgroundColor = String.new(color_hex).to_color
@button.setTitle(color_hex, forState: UIControlStateNormal)
end
end
This seemingly more complex task takes fewer lines of code than setting up our views. Run rake
and check out the fruits of our labor:
We can make one more small change to start adding that signature iOS polish. Right now, the user doesn’t get any feedback while the HTTP request is loading, aside from the tiny network activity indicator in the top bar. We can improve that experience by changing the button title while that’s going on. It’s also a good idea to be fault tolerant and show an error if the Colr API returns some bad data. Let’s make those changes:
def application(application, didFinishLaunchingWithOptions:launchOptions)
…
@button.setTitle("Change Color", forState:UIControlStateNormal)
@button.setTitle("Loading…", forState:UIControlStateDisabled)
@button.setTitleColor(UIColor.lightGrayColor, forState:UIControlStateDisabled)
…
end
def button_tapped
@button.enabled = false
BubbleWrap::HTTP.get("http://www.colr.org/json/color/random") do |response|
color_hex = BubbleWrap::JSON.parse(response.body.to_str)["colors"][0]["hex"]
# check if bad data as returned
if color_hex and color_hex.length > 0
@box.backgroundColor = String.new(color_hex).to_color
@button.setTitle(color_hex, forState: UIControlStateNormal)
else
@button.setTitle("Error :(", forState: UIControlStateNormal)
end
@button.enabled = true
end
end
Let’s rake
one last time to see our better UX in action. Not too shabby for our first app, right? You can look at the source for this example on GitHub.
“An Object In Motion…”
We’ve created an iOS app with dynamic data and a solid user experience in under 40 lines of clean Ruby. Making this app in Objective-C is definitely possible, but we would have lost the readability and brevity that Ruby affords. We also could have done it using a hybrid Web and native app; however, once you venture out of the basic UI building blocks and into a feature-complete iOS app, perfectly replicating a native experience becomes incredibly difficult.
Is RubyMotion right for your next iOS project? If you don’t have much experience in Objective-C but want to try app development, Ruby definitely has a gentler learning curving. Or if you’re already using Ruby somewhere else in your stack, you might reap the benefits of code portability. What about porting an existing app? Well, RubyMotion allows for middle ground: you can export your Ruby code as a static library to use in an Objective-C app, or you can use existing Objective-C code in a RubyMotion app.
This is not to say that RubyMotion is perfect. Its biggest flaw is debuggability: when you hit a nasty bug originating in the RubyMotion compiler, you can’t do a whole lot to trace or fix it. Because RubyMotion is currently closed source, we’ll have to wait for these issues to be remedied by HipByte. But if you don’t do anything tricky or “magical” with your code, then this shouldn’t be a problem. Unfortunately, I run into these issues fairly often because those “fun” bits are what make Ruby so compelling to me.
Fundamentally, RubyMotion hasn’t completely done away the original Objective-C API; you’ll still need to learn the iOS SDK to make really great iOS apps. That in itself tends to make up 80% of the hard work of developing any mobile app. And if you need to whip something up for multiple platforms, RubyMotion definitely won’t make your life any easier; it’s still very nascent and has some maturing to do. But efforts to Ruby-fy Apple’s APIs using RubyMotion are yielding interesting new directions for iOS development. On a platform defined by constraints and restrictions, having more choice is never a bad thing.
Further Reading
- Developer Center, RubyMotion The official developer documentation for RubyMotion.
- iOS Developer Library, Apple RubyMotion uses the existing iOS SDK; it doesn’t provide any new classes out of the box. If you run into problems with your
UIView
s or other SDK classes, consult this. - @RubyMotion The official Twitter account, where RubyMotion responds to support requests and publicizes new RubyMotion libraries.
- RubyMotion Tutorials A community-curated database of RubyMotion tutorials and writing.
- MacRuby RubyMotion is technically a port of MacRuby to iOS. If you enjoy using Ruby for your iOS apps, then you might like writing Mac apps the same way.
(al) (km)