Using Core Data in iOS with RubyMotion
Learn how to use Core Data in a simple RubyMotion app. By Gavin Morrice.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Using Core Data in iOS with RubyMotion
30 mins
- Getting Started
- Adding the Tasks Button
- Adding the Tasks List Screen
- Populating the Tasks List with Real Data
- Installing CDQ
- Creating a Data Model
- Be the Boss of cdq
- CDQ Helper Methods
- Loading Tasks from the Database
- Creating New Tasks
- Selecting the Current Task
- Editing the Tasks List
- Where To Go From Here?
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.
- 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. - Next, in
tableView:cellForRowAtIndexPath:
you return an instance of EmptyCell -- you'll define this class in a moment. - 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. - Finally,
todays_tasks
is a helper method that loads all of the tasks from the database.Although you could easily just call
Task.all
throughout the various methods inTasksViewController
, 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.
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
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.
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:
Looking good!