TDD in iOS with Ruby Motion: Part II - Working with Data

Brian Sam-Bodden

In the first installment of this series we covered the foundations of iOS development with RubyMotion by building the beginnings of a To Do application in a Test-Driven fashion. We learned the basics of Views and Controllers by building a simple application in which we got as far as displaying data in a list backed by an array.

In this installment we will dive into the "M" behind MVC, and along the way we'll gain an understanding of the different ways to persist data in iOS with RubyMotion.

Where we've been so far...

Figure BSB-1 shows the state of the ToDo application as we left it at the end of Part I.

Figure 1. ToDo App

We built the first installment in a test-driven fashion, and Listing BSB-1 shows the final state of the test suite.

Data in Mobile Applications

In this era of social communication and interaction, isolated applications are mostly non-existent. Most mobile applications (especially those with collaboration features) keep the authoritative source of their data in an external service and keep a local user data cache that needs to be synchronized from time to time.

Data in iOS

Data persistence in iOS presents us with a paradox of choice. There are many mechanisms by which to persist data locally in an iOS application, including but not limited to:

NSUserDefaults: Is a key-value local storage, capable of storing both objects and primitive data types. With NSUserDefaults data is stored in what is known as the iOS "defaults system". The defaults system is not meant for storing sensitive data, heavy objects or large amounts of data.

NSCache: As the name implies, it is a cache that stores key-value pairs. NSCache automatically evicts objects in order to free up space in memory as needed. It can hold larger amounts of data than NSUserDefaults, but it should be only used as a cache.

Archives: Archives are a means to serialize an object graph into an ``architecture independent stream of bytes''. Archives are available in sequential and keyed archives backed by NSArchiver and NSKeyArchiver respectively.

RubyMotion, RubyGems and Bundler

One of the aspects that makes Ruby such a productive environment is the richness of its open source ecosystem. RubyGems provides access to thousands of prepackaged Ruby libraries for you to use in your applications, but since RubyMotion is a dialect of Ruby that is statically compiled, most regular Ruby gems won't work right out of the gate. Fortunately, iOS typically provides an appropriate counter-part to a pure Ruby gem that has been wrapped in ``RubyMotion goodness'' for your enjoyment. Gems for use in RubyMotion need to have been specifically created with RubyMotion's constraints in mind.

In traditional Ruby applications, developers make gems available to their applications by installing them on their systems using the gem command or by using Bundler to create an application-specific 'bundle' of gems. Then, in their application's Ruby code, they use the require method to have access to those libraries' classes and modules.

In RubyMotion, the require method is only allowed inside the project's Rakefile. Gems required in the Rakefile are compiled into the target executable in alphabetical order.

For our ToDo App we'll be using a few libraries that will allow us to follow 'The Ruby Way' of development. We'll use:

From the command line, run the bundle install command. You might need to install Bundler (gem install bundler). With the gems now available in the system, we'll need to require them in our Rakefile so that RubyMotion can compile them into the target executable (Listing BSB-3).

Guard Motion

We'll further automate our TDD loop by using Guard Motion, a RubyMotion specific extension to guard (https://github.com/guard/guard). Guard will watch for changes to our project and automatically run the specs. Let's start by adding a guard definition (Guardfile) by running the guard init motion command, as shown in Listing BSB-4.

Motion Model

Now that we have our TDD loop automated, let's dive into crafting the models for our ToDo application. We'll approach our model development using the MotionModel library.

MotionModel provides a Ruby PORO (Plain Old Ruby Object) with light and simple persistence and validation abilities, similar to what ActiveRecord does in the Ruby on Rails world. MotionModel fits those cases where Core Data is too heavy, but you are still intending to work with your data, its types, and its relations. MotionModel's default persistence strategy relies on NSCoder to serialize data. It then uses NSKeyedArchiver to create a NSData representation.

Developing the Todo Model

We'll start to TDD our models at the lowest level with a simple spec to test that the Todo model exists, as shown in Listing BSB-6.

Listing BSB-6: Todo model existence test

describe "Todo Model" do
it "exists" do
Object.const_defined?('Todo')
end
end

The moment we save the file, we should see the build kick off, which should fail as shown in Listing BSB-7.

Let's start by creating an empty Todo model under app/models in the file todo.rb, as shown in Listing BSB-8.

Listing BSB-8: Todo model skeleton

class Todo
end

We can now enhance our tests to define what we expect from our Todo model. We'll start by specifying that the Todo model should know its name, description, due_date, and whether it is done or not, as shown Listing BSB-9.

Listing BSB-9: Todo model spec

before do
@todo = Todo.new
end
it "exists" do
Object.const_defined?('Todo').should.be.true
end
it "has a name, description, a due date and whether is done or not" do
@todo.should.respond_to :name
@todo.should.respond_to :description
@todo.should.respond_to :due_date
@todo.should.respond_to :done
end

To pass the specification, we'll mix in the main MotionModel functionality contained in MotionModel::Model, use the default persistence adapter provided by the module MotionModel::ArrayModelAdapter, and define our four columns as shown Listing BSB-10.

Let's round out the Todo model development with a couple of specifications: ``A Todo is not done by default'' and ``A Todo knows if its overdue.'' The final specification is shown in Listing BSB-13.

Listing BSB-13: Full Todo Model Spec

describe "Todo Model" do
before do
@now = NSDate.new
@todo = Todo.new :name => "Buy Milk",
:description => "We need some Milk",
:due_date => @now
end
it "exists" do
Object.const_defined?('Todo').should.be.true
end
it "has a name, description, a due date and whether is done or not" do
@todo.should.respond_to :name
@todo.should.respond_to :description
@todo.should.respond_to :due_date
@todo.should.respond_to :done
end
it "is invalid without a name" do
@todo.name = nil
@todo.should.not.be.valid
end
it "is not done by default" do
@todo.done.should.not.be.true
end
it "knows if its overdue" do
@todo.should.be.overdue
end
end

Our refactoring will involve replacing the @data string array with a collection of Todo model instances. Notice the call to Todo.all inside the refactored viewDidLoad method, which retrieves all existing Todos from storage. Also, in the tableView:numberOfRowsInSection method, we return the size of the @todos array and in tableView:cellForRowAtIndexPath we access the name property of the model to be displayed on the table. The refactored controller is shown in Listing BSB-16.

Let's refactor the specs to also make use of the Todo model (Listing BSB-18), and let's also rename the file todos_controller_spec.rb to reflect its purpose better.

Listing BSB-18: Refactored TodosController Specs

describe "Todos Controller" do
tests TodosController
before do
Todo.delete_all
@todo = Todo.create(:name => 'Buy Milk',
:description => 'Get some 1% to rid yourself of the muffin top',
:due_date => '2013-03-31')
@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

Now, with all of our tests passing, let's try launching the application to see where we stand. You'll quickly notice that our table of Todos is empty. Let's add a bit of code to our AppDelegate to seed the database on application startup (Listing BSB-19).

Todo Detail View

Now we can move on to developing the detail view for our Todos. The idea is that users will be able to select an entry from the Todos list and see a form displaying the Todo's details. That functionality will be the responsibility of the (yet to be created) TodoController (not to be confused with the existing TodosController). Let's start with the spec shown in Listing BSB-20.

The spec in Listing BSB-20 reveals a lot of implementation details that with a more mature testing framework you wouldn't need to know in advance. First, we are overriding the controller method of the spec to return a properly initialized TodoController instance (thanks to Clay Allsopp for the tip). Then we are assuming that we have handles to each one of the rows (which we'll create with Formotion) in order to check the values displayed in said rows.

Let's start by enhancing the Todo model with the Formotion capalities by mixing in MotionModel::Formotion as shown in Listing BSB-21.

Our first attempt at creating a skeleton for the TodoController is shown in Listing BSB-23. Formotion's central concept is that of a ``form,'' which is a collection of sections with rows. In the constructor of the TodoController, we are passing a Todo model instance to initialize the form. Luckily for us, MotionModel provides the #to_formotion method on a model that can be used to initialize a Formotion form.

Integrating the TodoController and TodosController

Integration of our two existing controllers will entail responding to a tap on the Todos table and displaying the TodoController view. To do so, we will modify the TodosController to handle the tableView#didSelectRowAtIndexPath event, and in it we will create a new instance of our TodoController using the selected Todo model and ``push'' it into the view using the #pushViewController method of the controller's navigationController as shown in Listing BSB-24.

To pass our tests, we need to get ahold of the ``rows'' in our form. Let's modify todo_controller_spec.rb and add a before block to grab ahold of the @form instance variable and extract the four visual rows of our form, as shown in Listing BSB-25.

If we launch the application using the rake command and select a given todo, we should see the TodoController in action, as shown in Figure BSB-2.

Figure 2. Showing a Todo's Details

Editing and Saving a Todo

So far, we have a storage-backed list of Todos that we can display on a table, and we can navigate to view individual Todo details. Now, let's add the ability to save any modifications made to a Todo. We'll start with the simple Spec shown in Listing BSB-26.

If you run the Spec, you'll notice that it fails when trying to call the non-existent #save method of the controller. MotionModel provides the reciprocal of the #to_formotion method in the #from_formotion! method, which we will use in the implementation of the #save method. In the #save method, we will grab the data from the form by calling the #render method and then update the value in the instance variable @todo (using #from_formotion!). Finally, we will save the modified model and, if we are not in test mode, navigate back to the Todos list (Listing BSB-27).

Exposing the save functionality to the users

We have tests showing that we can edit and save a Todo, but if we launch the application, you'll notice that there is no way for the user to ``save'' an edited Todo. Let's add a "Save" button to the right of our navigation bar, as shown in Listing BSB-28.

If we launch and test the application, we can see that the changed values are persisted when we tap the ``save'' button (Figure BSB-3), but they are not reflected on the Todos list.

Figure 3. Save Button

Model Motion supports notifications that are issued on object save, update, and delete. We can use the NSNotificationCenter default instance to register an observer, which will invoke the #todoChanged method, as shown in Listing BSB-29.

In the implementation of the #todoChanged method (Listing BSB-30), we get a notification object that we can interrogate for the 'action' triggered in the model object. In our particular case, we care about the 'update' action, which when triggered we will trigger a row reload in order to update the label shown on the table for a Todo.

Conclusion

In this installment we learned a bit about data persistence in iOS and how RubyMotion libraries can bring the simplicity of Rails-like DSLs into the world of iOS. The next steps for our RubyMotion ToDo App are to complete the Todo CRUD functionality and then to add the features that will make it a production-ready application.