Thursday, 2 August 2012

Get Started Writing iOS Apps With RubyMotion



Advertisement
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 appsusing 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.
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.
(Smashing's side note: Have you already bought the brand new Smashing Book #3? The book introduces new practical techniques and a whole new mindset for progressive Web design. Get your book today!)

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:
01$ motion
02Usage:
03  motion [-h, --help]
04  motion [-v, --version]
05  motion <command> [<args…>]
06 
07Commands:
08  create       Create a new project
09  activate     Activate the software license
10  update       Update the software
11  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:
1$ motion create Smashing
2    Create Smashing
3    Create Smashing/.gitignore
4    Create Smashing/Rakefile
5    Create Smashing/app
6    Create Smashing/app/app_delegate.rb
7    Create Smashing/resources
8    Create Smashing/spec
9    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 the rake 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 in Rakefile.
  • ./app/app_delegate.rb
    This is your only piece of code right now. It contains your app’s delegate. We’ll go into more detail soon, but know that every RubyMotion project needs a delegate.
  • ./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 the rake 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:
1# -*- coding: utf-8 -*-
2$:.unshift("/Library/RubyMotion/lib")
3require 'motion/project'
4 
5Motion::Project::App.setup do |app|
6  # Use 'rake config' to see complete project settings.
7  app.name = 'Smashing'
8end
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 theunshift, 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 ofrake “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:
01$ rake --tasks
02    rake archive              # Create archives for everything
03    rake archive:development  # Create an .ipa archive for development
04    rake archive:release      # Create an .ipa for release (AppStore)
05    rake build                # Build everything
06    rake build:device         # Build the device version
07    rake build:simulator      # Build the simulator version
08    rake clean                # Clear build objects
09    rake config               # Show project config
10    rake ctags                # Generate ctags
11    rake default              # Build the project, then run the simulator
12    rake device               # Deploy on the device
13    rake simulator            # Run the simulator
14    rake spec                 # Run the test/spec suite
15    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:
iOS Simulator and terminal when first running RubyMotion
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:
1$ rake
2  
3 
4(main)> "a string"
5=> "a string"
6(main)> h = {hello: "motion"}
7=> {:hello=>"motion"}
8(main)> h
9=> {: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:
Box color changed via server data
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::
1class AppDelegate
2  def application(application, didFinishLaunchingWithOptions:launchOptions)
3    true
4  end
5end
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 delegateobject. 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. Theapplication: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:
01class AppDelegate
02  def application(application, didFinishLaunchingWithOptions:launchOptions)
03    # UIScreen describes the display the app is running on.
04    app_frame = UIScreen.mainScreen.applicationFrame
05    @window = UIWindow.alloc.initWithFrame(app_frame)
06 
07    # This is the special method of UIWindow which lets them exist outside of a parent view
08    @window.makeKeyAndVisible
09 
10    # This is our blue box
11    # CGRectMake == CGRectMake(x, y, width, height)
12    @box = UIView.alloc.initWithFrame CGRectMake(0, 0, 100, 100)
13    @box.backgroundColor = UIColor.blueColor
14    @window.addSubview(@box)
15 
16    # UIButtonTypeRoundedRect is the standard button style on iOS
17    @button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
18    # A button has multiple "control states", like disabled, highlighted, and normal
19    @button.setTitle("Change Color", forState:UIControlStateNormal)
20    # Sizes the button to fit its title
21    @button.sizeToFit
22    # Position the button below our box.
23    @button.frame = CGRectMake(0, @box.frame.size.height + 20,@button.frame.size.width, @button.frame.size.height)
24    @window.addSubview(@button)
25 
26    true
27  end
28end
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 viewproperty 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:
Box views added to our RubyMotion app
Now we need to make @button actually do something besides turn blue when we press it. Buttons can take targets 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:
01    
02    @window.addSubview(@button)
03 
04    @button.addTarget(self, action:"button_tapped", forControlEvents:UIControlEventTouchUpInside)
05 
06    true
07  end
08 
09  def button_tapped
10    puts "I'm tapped!"
11  end
12end
UIControlEventTouchUpInside is one of many UIControlEvents 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:
1$ rake
2     Build ./build/iPhoneSimulator-5.1-Development
3   Compile ./app/app_delegate.rb
4      Link ./build/iPhoneSimulator-5.1-Development/Smashing.app/Smashing
5    Create ./build/iPhoneSimulator-5.1-Development/Smashing.dSYM
6  Simulate ./build/iPhoneSimulator-5.1-Development/Smashing.app
7(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:
1
2require 'motion/project'
3require 'bubble-wrap'
4
This makes the BubbleWrap::HTTP and BubbleWrap::JSON libraries available to us. The defaultBubbleWrap 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’sbackgroundColor to that value. In AppDelegate, let’s add the code to make this HTTP call in our button callback:
1def button_tapped
2  BubbleWrap::HTTP.get("http://www.colr.org/json/color/random") do |response|
3    color_hex = BubbleWrap::JSON.parse(response.body.to_str)["colors"][0]["hex"]
4    # ensure that color_hex is a String when we run .to_color
5    @box.backgroundColor = String.new(color_hex).to_color
6    @button.setTitle(color_hex, forState: UIControlStateNormal)
7  end
8end
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:
Box color changed via server data
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:
01def application(application, didFinishLaunchingWithOptions:launchOptions)
02  
03  @button.setTitle("Change Color", forState:UIControlStateNormal)
04  @button.setTitle("Loading…", forState:UIControlStateDisabled)
05  @button.setTitleColor(UIColor.lightGrayColor, forState:UIControlStateDisabled)
06  
07end
08 
09def button_tapped
10  @button.enabled = false
11 
12  BubbleWrap::HTTP.get("http://www.colr.org/json/color/random") do |response|
13    color_hex = BubbleWrap::JSON.parse(response.body.to_str)["colors"][0]["hex"]
14    # check if bad data as returned
15    if color_hex and color_hex.length > 0
16      @box.backgroundColor = String.new(color_hex).to_color
17      @button.setTitle(color_hex, forState: UIControlStateNormal)
18    else
19      @button.setTitle("Error :(", forState: UIControlStateNormal)
20    end
21    @button.enabled = true
22  end
23end
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.
Loading user experience

“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 UIViews 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)
Clay Allsopp is a hacker, Thiel Fellow, and internet enthusiast. An iOS developer since day one, Clay has crafted beautiful mobile apps with over a million cumulative downloads. He currently writes about mobile startups and is working on one of his own.

No comments:

Post a Comment