Using Core Data in iOS with RubyMotion

Learn how to use Core Data in a simple RubyMotion app. By Gavin Morrice.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Creating a Data Model

Open schemas/0001_initial.rb in your text editor, and you'll see some example code that's been commented out. Delete that and add the following in its place:

schema "0001 initial" do

  entity "Task" do
    string :name, optional: false
  end

end

This code defines a simple entity named Task. A Task has a name (defined as a string), which is required for a Task record to be valid (it's not optional).

There's one more step to finish adding your Task model, and that's to create a class for it in your application. In your text editor, create a file named task.rb in the app/models directory and define Task like so:

class Task < CDQManagedObject

end

The CDQManagedObject superclass is provided by CDQ, and it provides some extra methods for querying and saving data to your database.

Build and run the app in the simulator so you can try out CDQ in the console:

rake device_name="iPhone 4s"

Still in Terminal, run the following command once the app launches:

(main)> Task.count

It should return the number of Tasks currently in the database (which is currently 0):

(main)> Task.count
=> 0

Create a new Task by typing the following:

Task.create(name: "My first task")

The output should look similar to this:

(main)> Task.create(name: "My first task")
=> <Task: 0x11750b10> (entity: Task; id: 0x1174d940 <x-coredata:///Task/t2FA02CA9-05AF-4BD5-BAEB-B46F21637D0C2> ; data: {
   name = "My first task";
})

Now when you call Task.count, you should see it return 1.

(main)> Task.count
=> 1

Quit the simulator (by pressing ctrl + c), and then build and run the app once again.

rake device_name="iPhone 4s"

Fetch the number of Tasks once again by running Task.count, it should return 1.

(main)> Task.count
=> 0

...it should, but it didn't! What happened?!

Be the Boss of cdq

CDQ won't commit any data to the database unless you tell it to, and you do that by calling cdq.save in your code. This is an important gotcha to keep in mind.

Your application will not persist data unless you call this method. This is a deliberate feature of cdq; by only making commits to the database when you've defined all of the changes to be committed, you'll improve the memory usage and performance of your app.

It's easy to forget about this extra step – especially if you're coming to iOS from Ruby on Rails.

So start again from the top by create a new Task:

(main)> Task.create(name: "This is my first task")
=> <Task: 0xb49aec0> (entity: Task; id: 0xb49aef0 <x-coredata:///Task/t41D7C6A6-806D-4490-90A6-19F1E61ED6C12> ; data: {
    name = "This is my first task";
})

But this time, run the following command afterwards:

(main)> Task.save
=> true

Now quit your application and then re-launch. Call Task.count, and you'll see it returns 1.

(main)> Task.count
=> 1

CDQ Helper Methods

CDQ offers a few other helpful methods to save and retrieve persisted data. Run the following commands in the console to see their results.

Fetch the first row:

(main)> Task.first
=> <Task: 0xb49aec0> (entity: Task; id: 0xb47a830 <x-coredata://B0AEB5CD-2B77-43BA-B78B-93BA98325BA0/Task/p1> ; data: {
    name = "This is my first task";
})

Check if there are any records:

(main)> Task.any?
=> true

Find rows that match given constraints:

(main)> Task.where(name: "This is my first task").any?
=> true    

(main)> Task.where(name: "This is my second task").any?
=> false

Delete a row:

(main)> Task.first.destroy
=> #<NSManagedObjectContext:0xad53ed0>

And most importantly, commit changes to the database:

(main)> Task.save
=> true

You'll use these methods again soon -- when you complete the tasks list feature.

Loading Tasks from the Database

The Tasks screen should show each of today's Tasks that are in the database. First, though, you should prepare for the initial condition when there are no tasks.

Add the following implementation In tasks_view_controller.rb:

class TasksViewController < UITableViewController  
  # 1
  # = UITableViewDelegate =

  def tableView(table_view, heightForRowAtIndexPath: index_path)
    todays_tasks.any? ? 75.0 : tableView.frame.size.height
  end
  
  # 2
  # = UITableViewDataSource =

  def tableView(table_view, cellForRowAtIndexPath: index_path)
    table_view.dequeueReusableCellWithIdentifier(EmptyCell.name)
  end

  # 3
  def tableView(table_view, numberOfRowsInSection: section)
    [1, todays_tasks.count].max
  end

  # 4
  private

  def todays_tasks
    Task.all
  end

end

Most of this code should seem pretty familiar to you by now, except for the last method, but I'll walk you through each of them.

Although you could easily just call Task.all throughout the various methods in TasksViewController, it's good practice to define a method like this to access each of the objects or collections you load from the database.

Having this single point of access is much DRY-er (Don't Repeat Yourself), and it means you only have to change the code once if you need to add extra constraints later.

  1. First, you define tableView:heightForRowAtIndexPath from the UITableViewDelegate delegate. If there are any tasks in the database, then the height of each cell should be 75 points, otherwise, a cell's height should be the entire height of the UITableView.
  2. Next, in tableView:cellForRowAtIndexPath: you return an instance of EmptyCell -- you'll define this class in a moment.
  3. The tableView:numberOfRowsInSection: method should return 1 if there are no tasks in the database, or the number of tasks if there are any. This ensures that if the list of tasks is empty, the app still displays one cell.
  4. Finally, todays_tasks is a helper method that loads all of the tasks from the database.

Now add this:

def viewDidLoad
  super
  tableView.registerClass(EmptyCell, forCellReuseIdentifier: EmptyCell.name)
end

You've just registered the EmptyCell view class with TasksViewController's tableView by defining viewDidLoad.

Before you build the app again, you'll also need to create the actual class for the empty table view cell, so create a new file in the app/views directory named empty_cell.rb, like this:

touch app/views/empty_cell.rb

Now add this…

class EmptyCell < UITableViewCell

end

to define the EmptyCell class as a subclass of UITableViewCell.

Build and run the app in your simulator once again with this command:

rake device_name="iPhone 4s"

Now when you tap the tasks button, you'll see a screen like this

Pomotion tasks controller with large empty cell

If you see multiple cells or an error message, make sure that you've deleted all of the Tasks from the database by calling Task.destroy_all followed by Task.save in the app console.

Wouldn't this be more user friendly if it had a title? Of course it would. In tasks_view_controller.rb, update the viewDidLoad method like so:

def viewDidLoad
  super
  self.title = "Tasks"
  tableView.registerClass(EmptyCell, forCellReuseIdentifier: EmptyCell.name)
end

This time, when you build and run the app you should see the title Tasks in the navigation bar on the tasks screen.

Now you need to give the users a means of adding new tasks to the list. First, create an "Add" button with the following method:

def add_button
  @add_button ||= UIBarButtonItem.alloc.
    initWithBarButtonSystemItem(UIBarButtonSystemItemAdd, target: self, action: nil)
end

Then, add the following line at the bottom of viewDidLoad:

navigationItem.rightBarButtonItem = add_button

This, of course, adds the button to the right of your navigation bar. Build and run the app again, and you'll see a new + button that makes it possible to add new tasks.

Pomotion - Tasks list with "+" button

Most users will understand that the plus button is how you add tasks, but just to be on the safe side leave a hint that helps the user see how to add tasks.

To do this, simply add a message to the empty cell that tells the user how to add a task. Add the following method to empty_cell.rb:

def initWithStyle(style, reuseIdentifier: reuseIdentifier)
  super.tap do
    self.styleClass = 'empty_cell'
    textLabel.text = "Click '+' to add your first task"
  end
end  

Of course, the label needs a little styling before it's complete, otherwise, it'll be ugly. So, add the following CSS rules to resources/default.css

.empty_cell label {
  top: 200px;  
  left: 0;  
  width: 320px;
  height: 30px;
  font-size: 18px;
  color: #AAAAAA;
  text-align: center;
}

Build and run the app again with rake device_name="iPhone 4s". The tasks screen should now look like this:

Tasks screen with empty cell plus prompt styled

Looking good!