TDD in iOS with Ruby Motion: Part I

Brian Sam-Bodden
  • March 2013
  • Ruby
  • IOS

The iPhone, iPad, and iPod Touch devices conceived by Apple have become prevalent fixtures of modern life. For developers accustomed to Apple’s Objective-C language, the transition from creating desktop applications to creating applications for mobile devices has been fairly smooth. If you are a Web developer used to working with dynamically-typed, lightweight languages, following agile practices like Test-Driven Development, and comfortable with a Unix Shell, then jumping into a development world with a (albeit more modern and more dynamic that say…Java) cousin of C++ and an IDE that looks like an F16 cockpit just doesn’t seem appealing.

Luckily for those renegades there is an alternative in RubyMotion, a commercial, Ruby-based toolchain for iOS that brings a Ruby on Rails style of development to the world of iOS application development.

In this series of articles, I’m aiming to show you how you can build iOS applications in a test-driven fashion while also learning about the core iOS frameworks.

What is RubyMotion?

RubyMotion is a version of the popular MacRuby distribution, modified to target iOS. While MacRuby relies on Objective-C’s garbage collector, targeting iOS with a dynamic, reflective language like Ruby required creating a subset of Ruby that could be statically compiled and provide C-style semantics for memory management under the covers. RubyMotion’s subset of Ruby is mostly about preserving Ruby’s syntax while adhering to the needs of the underlying Objective-C constructs.

Why RubyMotion?

Many have tried to pinpoint who RubyMotion’s target audience is. In my opinion, RubyMotion is aimed at developers familiar with the Ruby language, agile web frameworks such as Ruby on Rails, unencumbered text editors such as Vi/Vim, Emacs, TextMate, and SublimeText, and more importantly with practices/techniques like test-driven development (TDD), behavior-driven development (BDD) and the usage of expressive constructs such as DSLs over graphical tooling.

With RubyMotion, by default, UIs are built programmatically, rapidly switching between a simple editor and the command line, while in XCode you wire your applications visually. A totally different experience!

While researching this article, I searched the web looking for test-driven iOS tutorials and found very little, which lead me to believe that TDD is not a highly embraced or easy-to-follow practice in the iOS and XCode world.

Getting Started

iOS is the operating system that runs on iPhone, iPod Touch, and iPad devices. Apple provides the iOS SDK, which provides the tools and interfaces needed to interact with iOS' layered architecture (Figure BSB-1).

iOS Layered Architecture

Figure 1. iOS Layered Architecture

iOS provides a set of packages called Frameworks, which are dynamic shared libraries and resources that can be linked to your App.

Typically all iOS development happens inside of Apple’s XCode IDE using the Objective-C language, which is a general-purpose, object-oriented language based on C and Smalltalk. iOS applications are compiled binaries that are meant to be distributed via Apple’s App Store (although there are utilities like HockeyKit http://hockeykit.net and TestFlight https://testflightapp.com for over-the-air beta distribution, none of them will allow you to sign your application). The whole ecosystem is proprietary, and so is RubyMotion. At the time of this writing, RubyMotion does not provide a free trial, and it must be purchased at http://www.rubymotion.com.

Just like the creator of RubyMotion, I do not believe that Xcode makes for a good development experience, and although not terrible, Objective-C’s verbosity gets in the way of rapid progress.

To follow along with this article you’ll need a Mac running OS X 10.6 or higher and the iOS SDK. You’ll also need a suitable text editor (I use TextMate with the RubyMotion Bundle).

If you want to test the application on a physical device you will need an iPhone, iPad, or iPod Touch device. You will also need to register as an iPhone developer at http://developer.apple.com/iphone to receive an application signing certificate.

Rather than go through the basic installation steps, I will refer you to http://www.rubymotion.com/developer-center/guides/getting-started. If you can build the simple Hello application then you are ready to rock!

Hello World and the REPL

Let start by playing with what I think is one of the selling points of RubyMotion, the REPL (read—eval—print loop, toplevel or language shell). The REPL in RubyMotion is very similar to Ruby’s native IRB or the Ruby on Rails console.

Once you have launched a RubyMotion application, it provides a prompt where you can interact with the compiled RubyMotion application and with every class/module available in the RubyMotion interpreter.

Let’s check our RubyMotion version and start by creating a hello world application as shown in Listing BSB-1.

Listing BSB-1: checking our RubyMotion version and creating the hello app

/> motion -v
1.32
          
/> motion create hello
  Create hello
  Create hello/.gitignore
  Create hello/Rakefile
  Create hello/app
  Create hello/app/app_delegate.rb
  Create hello/resources
  Create hello/spec
  Create hello/spec/main_spec.rb

RubyMotion creates 4 artifacts and a myriad of directories. For those used to Ruby on Rails, you can see the fingerprints of the Rails directory structure. Let’s talk about the 4 artifacts created by the motion create command:

  • .gitignore: Rubyists are fond of the Git Versioning Control System (VCS) http://git-scm.com/. This file tells Git not to include certain artifacts based on patterns.

  • Rakefile: This contains the build file for the system. Rake is the Make/Ant of the Ruby World. You can add your own tasks in this file, but by default this file configures the main App object.

  • app_delegate.rb: The Ruby file that glues your custom code (your application) into the RubyMotion framework.

  • main_spec.rb: A simple test that verifies that your app has at least one window.

Let’s start our exploration of RubyMotion by changing directories to the root of the newly created application (hello) and see what tasks are available via the rake -T command (Listing BSB-2).

Listing BSB-2: available Rake tasks

/> rake -T
rake archive               # Create an .ipa archive
rake archive:distribution  # Create an .ipa archive for distribution(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                  # 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

The default task (which will run if we simply type rake), builds the project (into the build directory) and runs the simulator. Let’s do that. You’ll find the output in Listing BSB-3.

Listing BSB-3: output from rake

/>rake
   Build ./build/iPhoneSimulator-6.1-Development
 Compile ./app/app_delegate.rb
  Create ./build/iPhoneSimulator-6.1-Development/hello.app
    Link ./build/iPhoneSimulator-6.1-Development/hello.app/hello
  Create ./build/iPhoneSimulator-6.1-Development/hello.app/Info.plist
  Create ./build/iPhoneSimulator-6.1-Development/hello.app/PkgInfo
  Create ./build/iPhoneSimulator-6.1-Development/hello.dSYM
warning: no debug symbols in executable (-arch i386)
Simulate ./build/iPhoneSimulator-6.1-Development/hello.app
(main)>

Our empty application should be launched in the simulator. It doesn’t have any views yet, so all we get is a black screen as shown in Figure BSB-2.

the hello app running

Figure 2. the hello app running

If you press the home button, you can see that our hello application is now installed on the simulator (Figure BSB-3).

application installed on the emulator

Figure 3. application installed on the emulator

In Listing BSB-2 you might have noticed that after the application launched, we were left with a prompt on the console. That’s the REPL my friend. Let’s follow the interaction in Listing BSB-3.

Listing BSB-4: REPL away!

(main)> self
=> main
(main)> alert = UIAlertView.new
=> #
(main)> alert.title = 'RubyMotion'
=> "RubyMotion"
(main)> alert.message = 'RubyMotion is that da house!'
=> "RubyMotion is that da house!"
(main)> alert.show
=> #
(main)>

The top level object is main like in most Ruby interpreters. Also, RubyMotion classes are available which are the counterparts/wrappers for their iOS peers. For example, in Listing BSB-3 we are constructing a UIAlertView. UIAlertView is an iOS class which is part of the UIKit Framework.

We can learn more about UIAlertView in Apple’s developer library at http://developer.apple.com/library/ios/#documentation/uikit/reference/UIAlertView_Class/UIAlertView/UIAlertView.html.

The UIAlertView has a few properties, including title and message, and several methods, including #show, that we used in Listing BSB-3. These interactions result in the alert shown in Figure BSB-04.

UIAlertView launched from the REPL

Figure 4. UIAlertView launched from the REPL

Next, as in Listing BSB-5, lets #dismiss the alert, add a button, change the message, and #show it one more time.

Listing BSB-5: REPL away!

(main)> alert.dismiss
=> #
(main)> alert.addButtonWithTitle 'Kaboom!'
=> 0
(main)> alert.message = 'Foo Bar'
=> "Foo Bar"
(main)> alert.show
=> #

Figure BSB-05 shows the result of invoking UIAlertView#addButtonWithTitle.

UIAlertView#addButtonWithTitle

Figure 5. UIAlertView#addButtonWithTitle

RubyMotion’s REPL gives us a laboratory where we can experiment with and discover the world of iOS constructs through the veneer of Ruby and RubyMotion. As we continue our exploration of RubyMotion, we’ll keep on coming back to the REPL for help.

Enhancing the hello App

Let’s change our hello App, adding some of the code that we used in the REPL, as shown Listing BSB-6.

Listing BSB-6: AppDelegate

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    alert = UIAlertView.new
    alert.title = 'RubyMotion'
    alert.message = 'RubyMotion is that da house!'
    alert.addButtonWithTitle 'Kaboom!'
          
    alert.show
          
    true
  end
end

Inside of the #application method, we can create objects that inherit from UIView (as UIAlertView does). UIView's define an interface for managing the content of a rectangular area of the screen.

At runtime our application is represented by a UIApplication object. This object “delegates” to your custom code, whose entry point is the AppDelegate class. In the world of iOS, a delegate is a class that typically implements one or more protocols. Protocols are contracts, represented as one or more method signatures (similar to an interface in the Java world).

With Ruby, all we need to do is provide a class (it doesn’t have to inherit from anything) and add the methods that we want to be delegated to our class. Nearly all UI classes have a delegate that you can use to receive callbacks from the framework.

If you have been writing Ruby code, you’ll notice that the method signature of #application looks a little strange. The second parameter is camel-cased and it has a colon smack in the middle. The camel-casing is just a direct port of the Objective-C parameter names. RubyMotion is mostly based on Ruby 1.9. In the upcoming Ruby 2.0, we get named parameters which use the : syntax, aligning perfectly with Objective-C parameters. Objective-C, however, does not really have named parameters. It looks like it does, but the colon is really just part of the signature. Unlike true named parameters, you can’t change the order of them in Objective-C.

In Listing BSB-5, the variable alert becomes a local variable to the method. Let’s make it into an instance variable by adding a @ in front of it, as shown in Listing BSB-7.

Listing BSB-7: Exposing the alert to the REPL

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @alert = UIAlertView.new
    @alert.title = 'RubyMotion'
    @alert.message = 'RubyMotion is that da house!'
    @alert.addButtonWithTitle 'Kaboom!'
          
    @alert.show
          
    true
  end
end

alert will become an instance variable of the delegate, and we can reach it via the application as shown in Listing BSB-8.

Listing BSB-8: Getting to alert via the REPL

(main)> app = UIApplication.sharedApplication
=> #
(main)> delegate = app.delegate
=> #>
(main)> alert = delegate.instance_variable_get('@alert')
=> #
(main)> alert.message = 'Chunky Bacon is the best!'
=> "Chunky Bacon is the best!"
(main)> alert.show
=> #

We can access the singleton application instance via the #sharedApplication class method, which in turn gets us access to the delegate, which we can get to by using instance_variable_get.

Figure BSB-6 shows the result of the interaction in Listing BSB-8.

Accessing the App and its delegate

Figure 6. Accessing the App and its delegate

TDD with Bacon

Ok, enough with the hello-worldish stuff. At this point we have a better idea of how iOS applications work and a suspicion that RubyMotion simplifies their development. Let’s now use the power of TDD to learn more about the platform and start building a real application along the way. The ubiquitous “ToDo” application.

RubyMotion comes bundled with MacBacon (https://github.com/alloy/MacBacon), a Mac specific version of Bacon (https://github.com/chneukirchen/bacon), which is itself a small clone of the popular BDD (Behavior-Driven Development) library RSpec (http://rspec.info/).

We’ll start our Todo app by accomplishing something very simple: we’ll populate a UITableView with data from an array of objects representing ToDos. We create our application scaffolding in Listing BSB-9.

Listing BSB-9: Creating the Todo application

/> motion create Todo
  Create Todo
  Create Todo/.gitignore
  Create Todo/Rakefile
  Create Todo/app
  Create Todo/app/app_delegate.rb
  Create Todo/resources
  Create Todo/spec
  Create Todo/spec/main_spec.rb 

Let’s start by revisiting the main_spec.rb, the simple Bacon test that verifies that your app has at least one window (Listing BSB-10).

Listing BSB-10: main_spec.rb

describe "Application 'hello'" do
  before do
    @app = UIApplication.sharedApplication
  end
          
  it "has one window" do
    @app.windows.size.should == 1
  end
end

If we change into the application directory and type rake spec, we’ll run the full suite of tests (currently only the one we just created), and it will fail since we don’t have a window (Listing BSB-11).

Listing BSB-11: Starting with Failure! A Good Thing

/>rake spec
    Build ./build/iPhoneSimulator-6.1-Development
  Compile ./app/app_delegate.rb
  Compile /Library/RubyMotion/lib/motion/spec.rb
  Compile /Library/RubyMotion/lib/motion/spec/helpers/ui.rb
  Compile ./spec/main_spec.rb
   Create ./build/iPhoneSimulator-6.1-Development/Todo_spec.app
     Link ./build/iPhoneSimulator-6.1-Development/Todo_spec.app/Todo
   Create ./build/iPhoneSimulator-6.1-Development/Todo_spec.app/Info.plist
   Create ./build/iPhoneSimulator-6.1-Development/Todo_spec.app/PkgInfo
   Create ./build/iPhoneSimulator-6.1-Development/Todo_spec.dSYM
 Simulate ./build/iPhoneSimulator-6.1-Development/Todo_spec.app
Application 'Todo'
  - has one window [FAILED]
          
Bacon::Error: 0.==(1) failed
  spec.rb:649:in `satisfy:': Application 'Todo' - has one window
  spec.rb:663:in `method_missing:'
  spec.rb:279:in `block in run_spec_block'
  spec.rb:403:in `execute_block'
  spec.rb:279:in `run_spec_block'
  spec.rb:294:in `run'
          
1 specifications (1 requirements), 1 failures, 0 errors 

Ok, so our app is expected to have one window. Let’s add one in our AppDelegate class (Listing BSB-12).

Listing BSB-12: Starting with Failure! The “Red” in Red-Green-Refactor

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.makeKeyAndVisible
          
    true
  end
end

Just like the UIAlertView that we used before, UIWindow is a type of UIView. UIWindow is a manager for other views your app displays on the device screen. An iOS application has only one window (unless your app can use an external display).

The code in Listing BSB-12 reveals some of the underlying Objective-C idioms. We alloc(ate) the memory (basically the same as #new) for our window and then initialize it with a frame that takes the dimensions of the device’s screen (via UIScreen.mainScreen.bounds).

Let’s run the tests again (Listing BSB-13) and see where we are.

Listing BSB-13: Passing the built-in tests: The “Green” in Red-Green-Refactor

/>rake spec
    Build ./build/iPhoneSimulator-6.1-Development
  Compile ./app/app_delegate.rb
     Link ./build/iPhoneSimulator-6.1-Development/Todo_spec.app/Todo
   Create ./build/iPhoneSimulator-6.1-Development/Todo_spec.dSYM
 Simulate ./build/iPhoneSimulator-6.1-Development/Todo_spec.app
2013-02-12 09:58:54.919 Todo[47912:c07] Application windows are expected to have a root view controller at the end of application launch
Application 'Todo'
  - has one window
          
1 specifications (1 requirements), 0 failures, 0 errors

Alright, now that we have passing set of tests and a very uninteresting application, let’s kick the TDD cycle into full gear by implementing a simple story:

Story 1: “As a user of the Todo application, I should be able to see a list of all Todos.”

Let’s craft a very generic test. To do so, we’ll follow the formula that we used to get access to things in our delegate from the REPL. In a before block, we’ll grab the application, the delegate, and something I’m calling table, which is yet to be created. Our first test will just test that this table exists in spec/todos_view_spec.rb, as shown in Listing BSB-14.

Listing BSB-14: A Very Generic Test for our Todos display

describe "ToDos View" do
  before do
    @app = UIApplication.sharedApplication
    @delegate = @app.delegate
    @table = @delegate.instance_variable_get("@table")
  end
          
  it 'should exist' do
    @table.should.not == nil
  end
end

Let’s run the tests again to get back to our “Red” state (Listing BSB-15).

Listing BSB-15: Back in the “Red”, testing for the Todos Table

...
Application 'Todo'
  - has one window
          
ToDos View
  - should exist [FAILED]
          
Bacon::Error: not nil.==(nil) failed
  spec.rb:649:in `satisfy:': ToDos View - should exist
  spec.rb:663:in `method_missing:'
  spec.rb:279:in `block in run_spec_block'
  spec.rb:403:in `execute_block'
  spec.rb:279:in `run_spec_block'
  spec.rb:294:in `run'
          
2 specifications (2 requirements), 1 failures, 0 errors 

To make our application pass this story we need to go into design mode. We’ll need a view that can display a list of things. Checking the descendants of UIView (http://www.rubymotion.com/developer-center/api/UIView.html), we see that there is a class called UITableView that looks like it should do the trick.

So how do we add this UITableView to our application’s window? If we inspect the methods of UIWindow in the REPL and filtering by the word view, we see that there is a method called addSubview (Listing BSB-16). Let’s expand our AppDelegate by creating the instance variable @table as a UITableView sized to the screen’s bounds.

Listing BSB-16: REPL detective work

(main)> UIWindow.instance_methods.sort.keep_if { |m| m =~ /\S*view\S*/ }
=> [:"addSubview:", :autoresizesSubviews, :"bringSubviewToFront:", :deliversTouchesForGesturesToSuperview, :"didAddSubview:", :didMoveToSuperview, :"exchangeSubviewAtIndex:withSubviewAtIndex:", :"insertSubview:above:", :"insertSubview:aboveSubview:", :"insertSubview:atIndex:", :"insertSubview:below:", :"insertSubview:belowSubview:", :layoutSubviews, :"movedFromSuperview:", :"movedToSuperview:", :removeFromSuperview, :"resizeSubviewsWithOldSize:", :"resizeWithOldSuperviewSize:", :"sendSubviewToBack:", :"setAutoresizesSubviews:", :"setClipsSubviews:", :"setDeliversTouchesForGesturesToSuperview:", :"setSkipsSubviewEnumeration:", :skipsSubviewEnumeration, :subviews, :superview, :viewDidMoveToSuperview, :viewForBaselineLayout, :viewPrintFormatter, :viewTraversalMark, :"viewWillMoveToSuperview:", :"viewWithTag:", :"willMoveToSuperview:", :"willRemoveSubview:"]

Listing BSB-17: Adding a UITableView

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.makeKeyAndVisible
          
    @table = UITableView.alloc.initWithFrame(UIScreen.mainScreen.bounds) 
    @window.addSubview(@table)
          
    true
  end
end

Running the tests reveals that we are back on green (Listing BSB-18). Although not a very robust test, it is allowing us to move forward in a controlled fashion.

Listing BSB-18: We’re back on “Green”.

...
Application 'Todo'
  - has one window
          
ToDos View
  - should exist
          
2 specifications (2 requirements), 0 failures, 0 errors

Let’s launch the application and see what we have cooking so far (Figure BSB-7).

Empty UITableView

Figure 7. Empty UITableView

The next thing we want to do is display some ToDos in our UITableView. If we look at the reference for UITableView there is a method #visibleCells that returns the table cells that are visible in the receiver. Specifically, it is n array containing UITableViewCell objects, each representing a visible cell in the receiving table view.

Let’s check the #visibleCells method on the REPL by getting to our @table UITableView (Listing BSB-19).

Listing BSB-19: We’re back on “Green”.

(main)> app = UIApplication.sharedApplication
=> #
(main)> delegate = app.delegate
=> # @table=#>
(main)> table = delegate.instance_variable_get("@table")
=> #
(main)> table.visibleCells
=> []

Excellent! Let’s use it to craft our next test, which checks that our table shows our Todos (Listing BSB-20).

Listing BSB-20: Checking that we have Todos

it 'displays the given ToDos' do
  @table.visibleCells.should.not.be.empty
end

Let’s run the tests and confirm that we have no Todos as of yet (Listing BSB-21).

Listing BSB-21: We ain’t got no Todos

Application 'Todo'
  - has one window
          
ToDos View
  - should exist
  - displays the given ToDos [FAILED]
          
Bacon::Error: not [].empty?() failed
  spec.rb:649:in `satisfy:': ToDos View - displays the given ToDos
  spec.rb:663:in `method_missing:'
  spec.rb:279:in `block in run_spec_block'
  spec.rb:403:in `execute_block'
  spec.rb:279:in `run_spec_block'
  spec.rb:294:in `run'
          
3 specifications (3 requirements), 1 failures, 0 errors

So how are we going to pass our test? It seems that we might want to get some UITableViewCell objects in our table.

If we inspect the UITableView reference (http://developer.apple.com/library/ios/#documentation/uikit/reference/UITableView_Class/Reference/Reference.html or http://www.rubymotion.com/developer-center/api/UITableView.html) we learn that a UITableView gets its data from a UITableViewDataSource (http://www.rubymotion.com/developer-center/api/UITableViewDataSource.html), which is set via the dataSource attribute.

The UITableViewDataSource protocol is adopted by an object that mediates the application’s data model for a UITableView object.

To create a class that can serve as a UITableViewDataSource we must provide two method implementations:

  • (Integer) tableView(tableView, numberOfRowsInSection:section): Tell the table how many rows of data we have.

  • (UITableViewCell) tableView(tableView, cellForRowAtIndexPath:indexPath): Returns a UITableViewCell for a given row index.

With that information at hand we can implement a Ruby class that would act as our data source. We will implement the two required methods in the contract, and serve our data from a simple array (Listing BSB-22).

Listing BSB-22: TodosDataSource

class TodosDataSource
          
  attr_writer :data
          
  def tableView(tableView, numberOfRowsInSection:section)
    @data.size
  end
          
  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, 
                                               reuseIdentifier:nil)
    cell.textLabel.text = @data[indexPath.row]
    cell
  end
end

For the time being, we’ll just slap the TodosDataSource class right below the AppDelegate in app_delegate.rb and enhance our AppDelegate to use it by simply setting its data attribute with an array of strings as the raw data to our data source.

Listing BSB-23: UITableView using TodosDataSource

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.makeKeyAndVisible
          
    @table = UITableView.alloc.initWithFrame(UIScreen.mainScreen.bounds) 
          
    todos = %w(Milk Orange\ Juice Apples Bananas Brocolli Carrots Beef 
               Chicken Enchiladas Hot\ Dogs Butter Bread Pasta Rice)
          
    todos.map! { |thing| "Buy #{thing}"}
          
    @data_source = TodosDataSource.new
          
    @data_source.data = todos
          
    @table.dataSource = @data_source
          
    @window.addSubview(@table)
          
    true
  end
end

Running the tests now results in the output found in Listing BSB-24.

Listing BSB-24: Passing “displays the given ToDos”

Application 'Todo'
  - has one window
          
ToDos View
  - should exist
  - displays the given ToDos
          
3 specifications (3 requirements), 0 failures, 0 errors

Let’s take a peek by “raking” the app. It should look like Figure BSB-08.

ToDos in our Table

Figure 8. ToDos in our Table

Let’s complement the previous test with a test to check the contents of the first rows added (Listing BSB-25).

Listing BSB-25: Passing “displays the given ToDos”

it 'displays the correct label for a give ToDo' do
  first_cell = @table.visibleCells.first
  first_cell.textLabel.text.should == 'Buy Milk'
end

Now we have a somewhat telling set of specs (Listing BSB-26).

Listing BSB-26: All “Green” and something to show

Application 'Todo'
  - has one window
          
ToDos View
  - should exist
  - displays the given ToDos
  - displays the correct label for a give ToDo
          
4 specifications (4 requirements), 0 failures, 0 errors

We’ve done a few red-green short loops; now let’s do a full red-green-refactor. You’ve probably noticed that when we launched the console we got a warning message that read:

2013-02-12 19:05:20.461 Todo[62741:c07] Application windows are expected to have a root view controller at the end of application launch

It turns out that iOS has a pretty robust implementation of the MVC model-view-controller pattern. If you remember when we access the ‘app’ (Listing BSB-27), we got an instance of UIApplication, also part of the UIKit framework, and the class that manages the application’s core behavior (including the event-dispatch loop). UIApplication has as its superclass UIResponder, which defines an interface for objects that respond to and handle events. It is the superclass of UIApplication, UIView, and its subclasses (which include UIWindow).

Listing BSB-27: UIApplication

(main)> app = UIApplication.sharedApplication
=> #

Figure BSB-09 depicts the MVC pattern as implemented in iOS. The first custom object created at launch time is the app delegate, which handles any events that are not handled by by the UIApplication. The app delegate is the entry point into the custom code that “controls” the app, the entry point into the “C” of M-V-C.

iOS MVC Core Objects

Figure 9. iOS MVC Core Objects

Let’s return to our message on the console. It means that we should have delegated the initial display of the view to a “view controller.” View controllers in RubyMotion are classes that extend UIViewController (see http://www.rubymotion.com/developer-center/api/UIViewController.html). Using views directly in the AppDelegate is typically frowned upon. Boo!

So let’s get with the times and refactor by using a view controller. There are many built-in subclasses of UIViewController tailored to simplify development. For our refactoring we’ll concentrate on UITableViewController, which knows how to host and manage an UITableView (see http://www.rubymotion.com/developer-center/api/UITableViewController.html).

We’ll start by making a directory for our controllers under the app directory. In there we’ll create the the TodosController in todos_controller.rb. I’m also, for the time being, moving our static data generation inside of the controller’s #viewDidLoad method (Listing BSB-28).

Listing BSB-28: TodosController

class TodosController < UIViewController
  attr_writer :data
          
  def viewDidLoad
    super
          
    self.title = "My ToDos"
          
    @table = UITableView.alloc.initWithFrame(self.view.bounds)
    self.view.addSubview(@table)
          
    @table.dataSource = self
          
    @data = %w(Milk Orange\ Juice Apples Bananas Brocolli Carrots Beef 
               Chicken Enchiladas Hot\ Dogs Butter Bread Pasta Rice)
               .map { |thing| "Buy #{thing}" }
  end
          
  def tableView(tableView, numberOfRowsInSection: section)
    @data.size
  end
          
  def tableView(tableView, cellForRowAtIndexPath: indexPath)
    cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, 
                                                         reuseIdentifier:nil)
    cell.textLabel.text = @data[indexPath.row]
    cell
  end      
end

Notice that the UITableViewDataSource methods are now part of the controller, and we simply set the UITableView data source to self (the instance of the controller). We’ll also need to refactor the AppDelegate to use the controller. We accomplish this by setting the window root view controller to an instance of our TodosController, as shown in Listing BSB-29.

Listing BSB-29: AppDelegate refactoring to use TodosController

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
          
    @todos_controller = TodosController.alloc
                        .initWithNibName(nil, bundle:nil)
          
    @window.rootViewController =
      UINavigationController.alloc
        .initWithRootViewController(@todos_controller)
          
    @window.makeKeyAndVisible 
          
    true
  end
end

If we run the tests, boom! We get one failure and two errors (Listing BSB-30).

Listing BSB-30: Boo, our refactoring broke our tests!

Application 'Todo'
  - has one window
          
ToDos View
  - should exist@Table is ==> 
  [FAILED]
  - displays the given ToDos@Table is ==> 
  [ERROR: NoMethodError]
  - displays the correct label for a give ToDo@Table is ==> 
  [ERROR: NoMethodError]
          
Bacon::Error: not nil.==(nil) failed
  spec.rb:649:in `satisfy:': ToDos View - should exist
  spec.rb:663:in `method_missing:'
  spec.rb:279:in `block in run_spec_block'
  spec.rb:403:in `execute_block'
  spec.rb:279:in `run_spec_block'
  spec.rb:294:in `run'
          
NoMethodError: undefined method `visibleCells' for nil:NilClass
  spec.rb:279:in `block in run_spec_block': 
    ToDos View - displays the given ToDos
  spec.rb:403:in `execute_block'
  spec.rb:279:in `run_spec_block'
  spec.rb:294:in `run'
          
NoMethodError: undefined method `visibleCells' for nil:NilClass
  spec.rb:279:in `block in run_spec_block': 
    ToDos View - displays the correct label for a give ToDo
  spec.rb:403:in `execute_block'
  spec.rb:279:in `run_spec_block'
  spec.rb:294:in `run'
          
4 specifications (2 requirements), 1 failures, 2 errors

The @table variable used in our tests is nil now. Well, of course! There is no @table in the delegate anymore. It’s in the controller now. Luckily for us, RubyMotion provides the ability to declare the context of our tests. In the refactoring in Listing BSB-31, we are using the class method #tests, passing the class of the controller we are testing (TodosController), which gives us access to the controller variable, which we can then use to get to our @table variable.

Listing BSB-31: Fixing our tests

describe "ToDos View" do
  tests TodosController
          
  before do
    @table = controller.instance_variable_get("@table")
  end
          
  it 'should exist' do
    @table.should.not == nil
  end
          
  it 'displays the given ToDos' do
    @table.visibleCells.should.not.be.empty
  end
          
  it 'displays the correct label for a give ToDo' do
    first_cell = @table.visibleCells.first
    first_cell.textLabel.text.should == 'Buy Milk'
  end
end 

With that change in place, all of our tests should now be passing again, and we now have a proper, tested MVC iOS App (Figure BSB-10).

MVC Todos List

Figure 10. MVC Todos List

Summary

With RubyMotion we can bring the amazingly successful practices of agile development, specifically TDD, into a world that is dominated by drag-and-drop, 4GL-like environments where testing is an afterthought. RubyMotion is an ideal vehicle for Ruby developers new to iOS to build applications in the same fashion we’re accustomed to using when building web applications.

In the next installment of the series, we’ll tackle the “M” in MVC, and use a proper storage-backed model in our Todo App rather than a simple array of strings. We’ll also jump into implementing full CRUD functionality for our Todos. Until the next time!

References

A good place to get clarity of RubyMotion’s features is http://www.rubymotion.com/support/#faq and the RubyMotion Google Group (http://groups.google.com/group/rubymotion) is a great resource when trouble-shooting issues, and as with most platforms, languages and frameworks StackOverflow http://stackoverflow.com/search?q=rubymotion is a perfect place to start your bug or API quirkiness hunt.

The RubyMotion Todo App code (with annotated commits) can be found at: https://github.com/integrallis/rm-todos

Share