Skip to content
Jeffrey Cutler edited this page Aug 27, 2014 · 7 revisions

MotionModel Tutorial

This goes on for a while, so here's a table of contents in case you want to skip ahead.

Setting Things Up

$ motion tutorial
$ cd tutorial

Now create a .gitignore so we can use git without getting a bunch of junk in our repository:

.repl_history
build
resources/*.nib
resources/*.momd
resources/*.storyboardc
.DS_Store
doc/**/*.*
doc
*.gem

Finally, let's add a few gems to sauce things up in a root directory file called Gemfile:

source "https://rubygems.org"

gem "rake"
gem "motion-cocoapods", "1.3.0.rc1"
gem 'bubble-wrap'
gem "sugarcube"
gem "motion_model", :git => "[email protected]:sxross/MotionModel.git"

We'll talk just a bit about these as the tutorial goes along, but the focus is on MotionModel, so you should refer to the documentation on each of them for more information.

A bit more housekeeping before I go on. The reason for choosing a Gemfile is so I can use Bundler, but letting Bundler install gems into the System Ruby directory is not necessarily a great idea. To localize my gems to a project, I use RVM. You can find out more about it here. It may look a little daunting, but once you're using it, you'll see that keeping your exact gem versions locked down using Bundler and RVM is quite useful. There are other tools than RVM for accomplishing this, but you should choose one. Once you have something in place for installing gems and Rubies someplace outside the System directory, install the most recent patch of Ruby 1.9.2. The reason for this is that RubyMotion is built on top of the 1.9.2 syntax.

Now, in your Rakefile right under the require for motion/project put this:

require 'bundler'

Bundler.require

And from the command line, do:

bundle install

If your system prompts you for a password, you didn't get RVM (or its substitute) installed right and/or active for this project.

Defining a Model

The first thing I typically do is sketch out my data structure. Some people coming from an IB background may start with UI mockups, but I prefer to figure out what I need to store and how. Let's start with a test:

# spec/models/task_spec.rb

describe 'Task' do
  it 'a Task can be instantiated' do
    Task.create.should.is_a Task
  end
end

Delete spec/main_spec.rb because it's really of no use and then:

$ rake spec

This, of course, fails with the error:

Task
  - can be instantiated [ERROR: NameError - uninitialized constant Task]

Of course, we don't yet have a Task model. So let's create one in app/models/task.rb.

class Task
end

and again, run the spec. Not the error has changed to:

Task
  - can be instantiated [ERROR: NoMethodError - undefined method `create' for Task:Class]

So we have Task class, but it doesn't have a create method. As create is the "C" in CRUD, we can assume that there's a part of model behavior missing. This is where MotionModel comes in.

class Task
  include MotionModel::Model
  include MotionModel::ArrayModelAdapter
end

What Have We Demonstrated?

By simply including the MotionModel modules and a persistence mechanism, in this case ArrayModelAdapter, we can turn our Ruby class into something that can behave as a model. But we only demonstrated it by the side effect of it being able to create a Task. Let's move on.

Adding "Columns" to a Model

I put "columns" in quotes because MotionModel is persistence-agnostic. That part is handled by its adapters. So the notion of columns is less accurate than "attributes". Here's how to add some:

# spec/models/task_spec.rb
# Add to your current spec

  it 'can create a task with a task_name attribute' do
    task = Task.create(task_name: 'Prepare Monthly Analysis of Sales')
    task.task_name.should == 'Prepare Monthly Analysis of Sales'
  end

As you would expect, there is another error:

  - can create a task with a task_name attribute [ERROR: NoMethodError - undefined method task_name=]

NoMethodError: undefined method task_name=

Of course! task_name wasn't part of the model. But how do we tell MotionModel we want this attribute to be part of the data model? Simple:

class Task
  include MotionModel::Model
  include MotionModel::ArrayModelAdapter

  columns :task_name => :string # <= Add this!
end

What Have We Demonstrated?

You can create a model with attributes -- one that can be persisted -- with one line of code. Ok, not really, we needed the other stuff too, but we only added one line to create an attribute that behaves as a model attribute.

Fleshing Out the Tasks Model

We're, of course, going to need some other attributes to adequately describe a task, so let's add a few.

  it 'has the attributes required of a Task' do
    task = Task.new
    [:task_name, :details, :due_date, :created_at, :updated_at].each do |attribute|
      task.should.respond_to attribute
    end
  end

Note: I'm not recommending you write a spec exactly like the one above. It does violate the principle of one test per example. But it's handy because you can require all the fields you need via spec in one fell swoop. Your testing methodology may differ. Of course, this one will fail because we haven't added these attributes, so we'll do that now.

class Task
  include MotionModel::Model
  include MotionModel::ArrayModelAdapter

  columns :task_name  => :string,
          :details    => :text,
          :due_date   => :date,
          :created_at => :date,
          :updated_at => :date
end

So there you have it -- a model that passes your specs. But what about adding a bit of functionality to the model. Let's say we need to know if a task is overdue.

Before we move on, let's add some other Ruby coolness in. This is why I love using Ruby -- all the cool stuff that makes my life easier. Open your Gemfile and put this line in:

gem 'motion-redgreen'

Then type the command:

$ bundle install

Now, open your Rakefile and add this to the config block:

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'tutorial'
  app.redgreen_style = :full # default: :focused  <= Add this
end

Now, when you type:

rake spec

you get a wonderful color-coded output that congratulates you for a job well done and reminds you when you need to fix something.

Let's add the spec for overdue?:

  it 'knows if a task is not overdue' do
    task = Task.create(task_name: 'Prepare Monthly Analysis of Sales', due_date: 1.day.hence)
    task.should.not.be.overdue
  end

  it 'knows if a task is overdue' do
    task = Task.create(task_name: 'Prepare Monthly Analysis of Sales', due_date: 1.day.ago)
    task.should.be.overdue
  end
Where'd 1.day.ago and 1.day.hence come from? In this case, SugarCube. Just so you know.

Now when you run:

$ rake spec

the results are quite different:

 - knows if a task is not overdue
 [✗] This test has not passed: ERROR: NoMethodError - undefined method overdue?!
 [ERROR: NoMethodError - undefined method overdue?]
 BACKTRACE: NoMethodError: undefined method overdue?
  model.rb:785:in `method_missing:': Task - knows if a task is not overdue
  spec.rb:663:in `block in method_missing:'
  spec.rb:646:in `satisfy:'
  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'


  - knows if a task is overdue
 [✗] This test has not passed: ERROR: NoMethodError - undefined method overdue?!
 [ERROR: NoMethodError - undefined method overdue?]
 BACKTRACE: NoMethodError: undefined method overdue?
  model.rb:785:in `method_missing:': Task - knows if a task is overdue
  spec.rb:663:in `block in method_missing:'
  spec.rb:646:in `satisfy:'
  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'

The failure is the same as without red-green, but the output is ANSI-colored to draw your attention to what failed. Of course, that's the overdue? method. You can now define this method in your model as follows:

  def overdue?
    due_date < Time.now
  end

You are free to write these "virtual attributes" in your code to provide calculated results and so on.

Hooking Your Model Up to a UITableViewController

Thus far, we haven't delved into making this a real iOS app. Here's where MotionModel really gets fun to use. First, let's put some skeleton code together to create a simple UITableViewController as the main window.

I'm deliberately not explaining much about the UI code because that's not what MotionModel is about. There are plenty of other places to learn about iOS coding, so bear with me.

The first file you have to flesh out it your app_delegate.rb. I'm going to put the code out here for you to work with.

class AppDelegate
  attr_reader :window

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

    @navigation_controller = UINavigationController.alloc.initWithRootViewController(@task_controller)
    @window.rootViewController = @navigation_controller
    @window.makeKeyAndVisible
    true
  end

  def load_data
    Task.create task_name: "Get a Driver's License",
                details:   "Learn how to drive, read the manual, go and pass the test",
                due_date:  "2013-08-16"
  end
end

There's no smoke and mirrors here. Just standard setup code. Except the call to load_data. That's where we will handle initializing the data MotionModel is managing. In this case, I'm just putting one task of seed data so we can see it works. As you can see, creating tasks can be done without assigning to a variable -- there is the concept of a data store independent of your application, much as there would be in an SQL database.

Next, we'll create controllers/tasks_view_controller.rb. This will be the controller that handles showing the tasks.

class TasksViewController < UITableViewController
  def viewDidLoad
    super
    self.tableView.dataSource = self.tableView.delegate = self
    @tasks = Task.all
  end

  def numberOfSectionsInTableView(view)
    1
  end

  def tableView(view, numberOfRowsInSection:section)
    Task.count
  end

  def tableView(view, cellForRowAtIndexPath:indexPath)
    cell = view.dequeueReusableCellWithIdentifier(cell_identifier)
    if not cell
      cell = UITableViewCell.alloc.initWithStyle UITableViewCellStyleSubtitle, reuseIdentifier:cell_identifier
    end
    task = @tasks[indexPath.row]
    cell.textLabel.text = task.task_name
    cell.detailTextLabel.text = task.details
    cell
  end

  def cell_identifier
    'task_cell'
  end
end

Let me step through this a bit. The way I look at viewDidLoad is that it's a great place to initialize things that are not view-related (i.e., they don't need iOS to set up a window size or do any layout). In this case, I'm loading all the tasks into an instance variable so I can quickly index it when I deliver cells in my dataSource.

In tableView:numberOfRowsInSection: I can just return Task.count to deliver the correct number of rows.

Finally, in tableView:cellForRowAtIndexPath:, I'm just using the array contained in the instance variable to populate the view.

So let's see it:

$ rake

And there you have it. An iOS app with a table view that has the one seed data item.

What Have We Learned?

That creating data items, counting them, and retrieving them is simple using MotionModel and that most of the code here focuses on iOS.

Now That You Have a Table, How Do You Add Data?

MotionModel issues notifications when you add, remove, or update data. You can observe these notifications. Why use notifications? For several reasons:

  • Notifications allow for loose coupling. Your app does not need to know anything about MotionModel, or in fact, what caused MotionModel to make the change. So, if you have a background thread that periodically pulls data from the Internet and adds it to your data store, MotionModel will issue the proper notifications to alert any controllers that are watching that these events happened.
  • Thread safe. Often, it is good to run expensive tasks like network data fetching, in a separate thread to keep the UI experience smooth. Notifications make sure that you are not depending on the data change happening in a particular thread, and the notification is issued only after the operation is complete, assuring you that it will be safe to read and/or write again.

How do we take advantage of notifications?

Let's modify the controller a bit:

MotionModelDataDidChangeNotification = 'MotionModelDataDidChangeNotification'

class TasksViewController < UITableViewController
  def viewDidLoad
    super
    self.tableView.dataSource = self.tableView.delegate = self
    @task_model_change_observer = App.notification_center.observe MotionModelDataDidChangeNotification do |notification|
      if notification.object.is_a?(Task)
        reload_data
      end
    end
  end

  def viewWillDisappear(animated)
    App.notification_center.unobserve @task_model_change_observer
  end

  def reload_data
    @tasks = Task.all
    self.tableView.reloadData
  end

  def numberOfSectionsInTableView(view)
    1
  end

  def tableView(view, numberOfRowsInSection:section)
    Task.count
  end

  def tableView(view, cellForRowAtIndexPath:indexPath)
    cell = view.dequeueReusableCellWithIdentifier(cell_identifier)
    if not cell
      cell = UITableViewCell.alloc.initWithStyle UITableViewCellStyleSubtitle, reuseIdentifier:cell_identifier
    end
    task = @tasks[indexPath.row]
    cell.textLabel.text = task.task_name
    cell.detailTextLabel.text = task.details
    cell
  end

  def cell_identifier
    'task_cell'
  end
end

I also made one modification to AppDelegate -- I moved the load_data method down to a point where the view will already have loaded and begun listening for notifications.

class AppDelegate
  attr_reader :window

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

    @navigation_controller = UINavigationController.alloc.initWithRootViewController(@task_controller)
    @window.rootViewController = @navigation_controller
    @window.makeKeyAndVisible
    load_data
    true
  end

  def load_data
    Task.create task_name: "Get a Driver's License",
                details:   "Learn how to drive, read the manual, go and pass the test",
                due_date:  "2013-08-16"
  end
end

I've included the whole class for the TasksViewController, but here are the highlights of the modifications:

  • Added an observer for the model change notifications using BubbleWrap's notification wrappers.
  • Made sure I'm only listening for Task data modifications.
  • Reload the data whenever there is a change.

I can rely on the notifications even for the data loaded initially because the view is loaded before I call load_data in AppDelegate.

Let's take it for a spin:

$ rake

Now we have an iOS app with the one seed data entry, as expected. Now what? In the REPL, type this (make sure you can see both your terminal window and the simulator):

(main)> Task.create :task_name => 'Enroll in Particle Physics Class', :details => "I may be an overachiever... don't know."
=> Task#2:0x9de4fe0

Watch and marvel as the new Task is added into your table view!

What Have We Learned?

Well, we learned that handling notifications is a gee-whiz, nifty way to keep your table view in sync with changes in MotionModel's data store.

About Adding New Data

At some point, you'll want to add some new data to your collection.

Let's pull in formation using our Gemfile to help us gather input. Add the following line to Gemfile:

gem 'formotion'

Because we're embedding our UITableViewController inside a UINavigationController, the logical place for the add button is on the navigation bar, so let's make that come true. Add the following method to your TasksViewController class:

  def on_add(sender)
    task = Task.new
    task_input_controller = TaskInputController.alloc.initWithForm(task)
    self.navigationController.pushViewController(task_input_controller, animated:true)
  end