Skip to content
This repository

Introduction

In this tutorial, we will use Nitron to implement a simple task manager using a storyboard-based workflow. We'll use the following features of Nitron:

  • data binding
  • outlets + actions
  • CoreData

We'll do this in an iterative fashion so you can get a feel for how things work. For users with more Ruby experience, it includes a crash course in using Xcode.

If you get stuck, or have questions, please create an Issue and we will get you sorted out. Thanks!

Prologue

As with many things in Cocoa and Cocoa Touch, your code has to be letter-perfect to get things to work properly (what? it isn't?). So, for example, if you misspell TaskViewController inside task_list_view_controller.rb, you will get an exception from RubyMotion. This is neither specific to RubyMotion, nor to Nitron. In fact, it's not endemic to using Interface Builder. It's the nature of the beast. Fortunately, the error messages are good enough that you can track things down pretty quickly.

If you get a "this class in to key value coding-compliant for the key XYZ", it's likely you forgot to implement a method on your model class.

Setting up the project

  1. First, verify you have the latest RubyMotion version:

    $ sudo motion update

  2. Next, let's create a new RubyMotion project called tasks:

    $ motion create tasks

  3. Create a new Xcode project called tasks as well. Select the Master-Detail template, then set up the project properties like so:

    Xcode configuration

    Ensure that the "Use Storyboards" and the "Use Core Data" checkboxes are selected.

  4. Right click on your project in Xcode, and select Show In Finder:

    Helpful screenshot

  5. Double-click on the Tasks folder, and you should see your Xcode project layout:

    Sample project layout

    Copy the MainStoryboard.storyboard (probably located in en.lproj) and the Tasks.xcdatamodeld files to your RubyMotion project's resources directory.

  6. Inside of Xcode, delete those two files. When prompted, click on Remove References.

  7. Open your RubyMotion project's resources directory, select those two files, and drag them back to Xcode. Ensure that the checkbox labeled "Copy items into destination group's folder" is not checked. (We want to relocate these files, after all!)

    Add files dialog

    Minimize Xcode, as we'll return to it later.

  8. Create a Gemfile alongside your Rakefile, and paste the following contents in:

    source :rubygems
    
    gem "rake"
    gem "nitron"
    
  9. Now, let's update our Rakefile to use Bundler for us. Add the following lines immediately before the Motion::Project::App.setup block:

    require 'rubygems'
    require 'bundler'
    
    Bundler.require
    

    These lines must come after the standard one that reads require 'motion/project'. From now on, we shouldn't have to add any additional require statements for RubyMotion-compatible gems.

  10. Since we updated our Gemfile, let's update our bundle:

    $ bundle

  11. Comment out/delete app/app_delegate.rb, as Nitron provides a boilerplate app_delegate.rb that will suffice for our needs.

  12. Compile the project, and verify that it launches properly:

    rake

    You should see a blank table view controller, with a title of "Master". Now, check the RubyMotion console. You should find a complaint from the SDK:

    2012-06-10 20:26:27.613 tasks[82692:fb03] Unknown class MasterViewController in Interface Builder file.

    The iOS SDK tried to instantiate an instance of a class named MasterViewController, but couldn't find it. Let's fix this error.

Listing tasks

  1. Create a new directory underneath app called controllers. In that directory, create a file named task_list_view_controller.rb and paste the following contents in:

    class TaskListViewController < Nitron::TableViewController
    end
    
  2. Go back to Xcode, and click on the MainStoryboard.storyboard file. On the middle navigation bar (containing "Navigation Controller Scene"), click on the orange icon labeled Master View Controller. On the right hand view (known as the Utilities view), click on the Identity tab (third one from the left, see the screenshot). You'll see a field labeled "Class". Set that to TaskListViewController, and press return. Verify that the label on the left (that used to say "MasterViewController") updates to say "TaskListViewController."

    Setting controller's class name

    Save the file, and minimize Xcode.

  3. Build your RubyMotion project again, and verify the error is gone. If it isn't, you may need to make a tweak or two. If you are supporting both iPhone and iPad (Universal), as Apple recommends, MainStoryboard.storyboard will not be present. Instead, you'll find MainStoryboard_iPhone.storyboard and MainStoryboard_iPad.storyboard. For this first step to work, rename MainStoryboard_iPhone.storyboard to MainStoryboard.storyboard and its analogous folder the same.

    The errors that indicate you need to make this tweak are:

    WARNING! File MainStoryboard_iPhone.storyboardc/12-view-13.nib' found in app bundle but not in./resources', removing
    WARNING! File MainStoryboard_iPhone.storyboardc/21-view-22.nib' found in app bundle but not in./resources', removing
    WARNING! File MainStoryboard_iPhone.storyboardc/Info.plist' found in app bundle but not in./resources', removing
    WARNING! File MainStoryboard_iPhone.storyboardc/UIViewController-12.nib' found in app bundle but not in./resources', removing
    WARNING! File MainStoryboard_iPhone.storyboardc/UIViewController-21.nib' found in app bundle but not in./resources', removing
    WARNING! File MainStoryboard_iPhone.storyboardc/UIViewController-3.nib' found in app bundle but not in./resources', removing

    For the time being, ignore these. When you switch back to Xcode, you'll notice MasterViewController_iPhone.storyboard is shown in red. That's because we renamed it. Delete that, go to the finder and drag the MainStoryboard.storyboard file to your Xcode project. That should fix the missing-file problem.

  4. Create a directory underneath app named models, and create a file named task.rb inside of it. Paste the following code in:

    class Task
      def self.all
        [Task.alloc.init, Task.alloc.init]
      end
    
      def title
        "Learn how to ride a tricycle"
      end
    end
    

    Notice how we're not worrying about persisting the model quite yet, we're just sketching it out. To support this, we've created an ActiveRecord-esque all method that returns a collection of Tasks.

  5. Return to Xcode, and select the TableView prototype cell shown in the storyboard view. The best way to do this is to expand the TaskListViewController tree on the middle panel to show the Table View item, expand that, and then click on Table View Cell. Change to the Attributes inspect (fourth tab on the Utilities bar), and change the Identifier to TaskListCell.

    Configuring table view cell's prototype

    Nitron deduces the table cell prototype's name by stripping both "View" and "Controller" from the controller name, then appending "Cell". For example: given a controller named MyCoolTableViewController, Nitron would expect a table cell prototype named MyCoolTableCell.

  6. Update TaskListViewController to tell it about the collection it should use:

    class TaskListViewController < Nitron::TableViewController
      collection { Task.all }
    end
    
  7. Run the simulator again. Notice how we have two items in the list, but they just say "Title". Let's use Nitron's data binding to make this nicer.

    Quit the simulator, and switch to Xcode. Select the cell's Label, and change to the Identity tab. In the User Defined Runtime Attributes section, click the + button to create a new attribute. Set the Key Path to dataBinding, the Type to String, and the Value to model.title.

    Save the Storyboard.

  8. Run the simulator again and view the results of your handiwork:

    Data binding result

    Looks like we need to find ourselves a tricycle.

    Notice how Nitron used the dataBinding attribute you set in Xcode to set up the to call the title method on each model in the collection. To prove that this is the case, let's update the Task model so we can set the title in the constructor:

    class Task
      def self.all
        [Task.alloc.initWithTitle("Learn how to ride a tricycle"), Task.alloc.initWithTitle("Buy a pony")]
      end
    
      attr_reader :title
    
      def initWithTitle(title)
        @title = title if init
        self
      end
    
    end
    
  9. Rebuild, and note your new directive:

    Data binding result #2

Viewing a task's details

  1. Try clicking on one of the cells in the simulator. Doing so should cause a crash.

    Sadly, when you click on the disclosure triangle and your app crashes (which is expected), the error message is not too helpful:

    2012-07-12 11:20:57.663 TaskChecklist[48368:fb03] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[
    setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key detailDescriptionLabel.'

    This is a bit of a bummer because fixing it relies on your knowing you just haven't implemented something yet. This is one place where using Interface Builder allows you to leave things unimplemented or simply not hooked up and getting unknown or undecipherable crashes. This isn't exactly about Motion, or Nitron -- it's just one of those "programming an iPhone app can be a challenge some days" kind of things.

    Let's fix the unimplemented bug.

  2. Return to Xcode, and select the Detail View Controller cell shown in the storyboard view. Change to the Identity inspector (third tab on the Utilities bar), and change the Class from DetailViewController to TaskDetailViewController.

Next, delete the single label in the view (it should be labeled "Detail view content goes here"). Add four labels and arrange in some manner like you see here:

Detail view layout

Use the Attributes Inspector to modify the text attribute of each left-most Label so it matches the screenshot.

To create a Label, use the drop down in the lower right to filter the object list by controls. Drag a label onto the detail view to create it. Find the label object

  1. Next, add data bindings for the rightmost Labels. For the upper right label, add a User-Defined Runtime Attribute named dataBinding with a type of String, and a value of model.title. For the lower right label, add a User-Defined Runtime Attribute named dataBinding with a type of String, and a value of model.due. (Don't worry, we'll write this method shortly.)

  2. In your RubyMotion project, create the corresponding controller in app/controllers/task_detail_view_controller.rb:

    class TaskDetailViewController < Nitron::ViewController
    end
    

    We're using just a plain ViewController here, since the view being presented is not a table view.

  3. Update the Task model to add the due method. We'll make each Task due right now to start:

    class Task
      def self.all
        [Task.alloc.initWithTitle("Learn how to ride a tricycle"), Task.alloc.initWithTitle("Buy a pony")]
      end
    
      attr_reader :title
    
      def initWithTitle(title)
        @title = title if init
        self
      end
    
      def due
        NSDate.date.to_s
      end
    
    end
    
  4. Run the simulator, and try clicking on an item. You should see a screen similar to this:

    Detail view layout

    The due date isn't very pretty. You can fix that yourself, if you'd like.

Moving to CoreData

  1. In your Xcode project, click on the Tasks.xcdatamodel file to open up the data modeler.

  2. Under Entities, delete the sample Event entity Xcode creates for you by selecting it, and pressing the Delete key.

  3. Click on the Add Entity button (near the bottom). Name the new entity Task.

  4. Under the Attributes section, create two attributes: Attribute Name: title Attribute Type: String Attribute Name: due Attribute Type: Date

    Xcode data modeler

  5. Save the Tasks.xcdatamodel file.

  6. Update the Task class definition to be:

    class Task < Nitron::Model
      # CoreData requires that every fetch be ordered somehow.
      def self.all
        order("due")
      end
    
      # We're using a text field to display a date when data binding.
      def due
        primitiveValueForKey("due").to_s
      end
    end
    
  7. Rebuild your project. You should see a blank TableView once again.

  8. Switch to the RubyMotion REPL, and type:

    Task.create(title: "Go running", due: NSDate.date)

    Hit enter. You should see:

    CoreData sample

    The magic of CoreData + NSFetchedResultsController (used internally by Nitron) is that these changes occur immediately without any extra code required by you. Click on the entry, and view the details page.

  9. Force quit the app in the simulator, then run it again to verify that the data was persisted.

Adding tasks

  1. Switch back to Xcode's storyboard view. Add a Bar Button Item (filter by "Windows & Bars") to the TaskListViewController with an Identifier of "Add". When adding the "Add" button, you may be confused when looking for a way to assign the identifier "add". It's in the Attributes Inspector.

  2. Change the filter on the Objects palette to "Controllers & Objects", then drag a new View Controller somewhere below the TaskListViewController. Set the class of the new ViewController to be TaskCreateViewController

  3. Hold down the control key, and drag from the Bar Button Item you added in step 1 to the TaskCreateViewController. When prompted, choose Modal. This wires the + button on TaskListViewController to show TaskCreateViewController modally.

  4. Create a controller for TaskCreateViewController under app/controllers defined as follows:

    class TaskCreateViewController < Nitron::ViewController
    end
    
  5. Try running your project and clicking the + button. You'll notice the TaskCreateViewController loads, but doesn't show anything. Worse, you can't dismiss it. Let's fix that.

  6. Add a Navigation Bar, and two Bar Button Items to the TaskCreateViewController. (You can add the Bar Button Items to both sides of the title.) Set the Identifier of the left Bar Button Item to be Cancel, and the Identifier of the right Bar Button Item to be Save. Also, edit the Title of the Navigation Bar so that it says "New Task". You should end up with something like this:

    New Task layout

  7. Now, we need to write an event handler for the Cancel button. First, we'll need to expose it to our controller using Nitron's outlet support. (This works in a manner similar to Xcode's outlets.)

    To do this, we will add a User-defined Runtime Attribute called outlet to the Cancel Bar Button Item, with a type of String, and a value of cancel.

    Wiring up an outlet

    This attribute is used by Nitron to create an accessor method within the TaskCreateViewController called cancel. The cancel method returns the view the outlet was specifed on.

  8. Now, we'll use Nitron's actions DSL to make use of our outlet. Update TaskCreateViewController:

    class TaskCreateViewController < Nitron::ViewController
      on :cancel do
        close
      end
    end
    
  9. Rebuild the app, and test the Cancel button out.

  10. In Xcode, add a TextField (filter by Controls) with an outlet named "titleField" and a DatePicker (filter by Data Views) with an outlet named "datePicker". It should look something like this:

    Wiring up an outlet

    It may not be pretty, but we'll make it work. (The title text field's dimmed out text is placeholder text that you can assign through Xcode).

  11. Next, add an outlet to the Save Bar Button Item called save.

  12. Update your controller:

    class TaskCreateViewController < Nitron::ViewController
      on :cancel do
        close
      end
    
      on :save do
        Task.create(title: titleField.text, due: datePicker.date)
    
        close
      end
    end
    
  13. Test out the Save button.

Something went wrong with that request. Please try again.