Realm database is made to save large amounts of data. We will use
our
superapp
as an example in this tutorial. It will save user's mood into Realm
database, and will retrieve all data from it to show user's
mood history in the table.
To add it to the
superapp
,
we need to do next:
git clone https://github.com/realm/realm-cocoa
sh build.sh build
;
realm-cocoa/build/ios/Realm.framework
into
superapp/vendor/realm
.
Then we need to configure
Rakefile
.
We will need to include next three things:
Realm.framework
libc++
,
which is a Realm's dependency.
superapp/schemas
for it.
# Realm dependency:
app.libs << '/usr/lib/libc++.dylib'
# Realm:
app.external_frameworks << 'vendor/realm/Realm.framework'
# Place where DB schemas will be:
app.vendor_project 'schemas', :static, :cflags => '-F ../vendor/realm/'
We could stop setting up Realm at this point - we have included
everything that we need, and Realm will work well. However, it is
a good idea to include a
motion-realm
gem that helps us to work with Realm in a more Ruby-like style. Let's
add it to
Gemfile
, and run
bundle install
after:
source 'https://rubygems.org'
gem 'rake'
# Add your dependencies here:
gem 'ProMotion'
gem 'sugarcube', :require => 'sugarcube-classic'
gem 'motion-kit'
gem 'motion-realm'
At this point, we have added
Realm
to our project, but we still have no DB schema. Schemas needed to
group logically objects inside of it. For now, our schema
will contain only one object - Mood. Mood objects will
have:
created_at
.
schemas/schema.m
:
// include Realm:
#import <Realm/Realm.h>
// Create Mood class description (interface)
// that uses RLMObject as a base class:
@interface Mood : RLMObject
// Mood class has created_at and (mood) level properties.
@property NSDate *created_at;
@property NSString *comment;
@property NSInteger level;
// end of a Mood interface
@end
@implementation Mood
@end
At this moment, we have a schema with one class - Mood. Let's create a
Mood model now in app/models/mood.rb
:
Class Mood
# include some helpful features of motion-realm gem:
include MotionRealm
end
Now we can launch the app. Nothing will be changed, but we can be sure
that it works now, and Realm has been included successfully, with all
its schemas and models.
Now we need to make
MoodSelectorScreen
work - the user should be able to show it as a modal screen from the
MoodHistoryScreen
. When the mood is selected,
we will create and persist a new Mood object.
First we need to add some button into Mood History Screen which will
allow the user to open Mood Selector Screen. We will do it by adding
UIBarButtonItem
to
MoodHistoryScreen
navigation bar:
class MoodHistoryScreen < PM::Screen
title "Mood History"
# MoodHistoryScreen code ...
def on_load
# on_load code ...
# create Add button, and place it to the navigation bar:
add_btn = UIBarButtonItem.titled('Add') { open_mood_selector }
self.navigationItem.setRightBarButtonItems [ add_btn ]
set_table_data
end
def open_mood_selector
screen = MoodSelectorScreen.new nav_bar: true,
presentation_style: UIModalPresentationFormSheet
open_modal screen
end
# MoodHistoryScreen code ...
end
Now each time you press "Add" button in Mood History Screen, it will
open modal
MoodSelectorScreen
.
We did not update our
MoodSelectorScreen
for a long time, so let's modify it a little bit. Let's set its title
to "Mood Selector", and add a "Close" button to the left side of the
navigation bar, that will hide the screen:
class MoodSelectorScreen < PM::Screen
title "Mood Selector"
def load_view
@layout = MoodSelectorLayout.new
self.view = @layout.view
end
def on_load
self.view.backgroundColor = 0xffffff.uicolor
# create Close button, and place it to the navigation bar:
close_btn = UIBarButtonItem.titled('Close') { close }
self.navigationItem.setLeftBarButtonItems [ close_btn ]
end
end
Now we need to make MoodSelectorScreen
work. Since we
are going to add
on_tap
method to our buttons, we need to require
sugarcube
's
sugarcube-gestures
package. We can do it as usual in
Gemfile
:
source 'https://rubygems.org'
gem 'rake'
# Add your dependencies here:
gem 'ProMotion'
gem 'sugarcube', :require => [ 'sugarcube-classic', 'sugarcube-gestures' ]
gem 'motion-kit'
gem 'motion-realm'
Now when the user presses on one of the buttons in
MoodSelectorScreen
,
we will call some method, and pass a mood level to it: if "Awesome"
button selected, the level will be 4. If "Good", then the level is 3, and so on.
Then we will create a Mood object, persist it, and close
MoodSelectorScreen
:
class MoodSelectorScreen < PM::Screen
title "Mood Selector"
def load_view
@layout = MoodSelectorLayout.new
self.view = @layout.view
end
def on_load
self.view.backgroundColor = 0xffffff.uicolor
# create Add button, and place it to the navigation bar:
close_btn = UIBarButtonItem.titled('Close') { close }
self.navigationItem.setLeftBarButtonItems [ close_btn ]
@layout.get(:awesome).on_tap { set_mood(4) }
@layout.get(:good).on_tap { set_mood(3) }
@layout.get(:average).on_tap { set_mood(2) }
@layout.get(:bad).on_tap { set_mood(1) }
end
def set_mood(level)
mood = Mood.create level: level, created_at: NSDate.now
RLMRealm.write do |realm|
realm << mood
end
close
end
end
Now we can launch the app after
bundle install
:
Try to create some mood objects, and let's try to browse our database in REPL:
Mood.count
# => returns number of Mood objects
mood = Mood.last
# => returns last mood object
mood.level
# => returns last mood level
mood.created_at
# => returns the date of a mood object creation
By the way, you can also download
Realm Browser, and use it to browse the DB. To get a path to your
Realm DB file, just use next command in REPL:
RLMRealm.default.path
So now when the user can finally create mood objects, we need to show
them somehow. Let's start with
MoodHistoryScreen
:
table should show all added mood objects. To do it
we will use Screen's delegate method
will_appear
that fires each time screen is going to be shown to the user.
Each time screen is going to be shown to a user, we will get
mood objects for a last week from the database, and will
transform them to a table
data pattern that is already present in the
MoodHistoryScreen
code:
def will_appear
set_table_data
end
def set_table_data
max_date = NSDate.now.end_of_day
min_date = (max_date - 6.days).start_of_day
predicate = NSPredicate.predicateWithFormat "created_at <= %@ AND created_at => %@", max_date, min_date
moods = Mood.with_predicate(predicate)
# group moods by day
grouped = moods.to_a.group_by do |mood|
mood.created_at.string_with_format("MMM dd")
end
# empty @data array:
@data = []
# populate data array. Iterate over grouped hash, where hash key
# will be `section_name`, and moods in a given group - `moods`
grouped.each do |section_name, moods|
# create empty section:
section = {
title: section_name,
cells: []
}
# iterate over all moods in a given group and generate
# table cells from mood objects:
moods.each do |mood|
# Figure out what part of the day was on the time of adding a mood:
mood_hour = mood.created_at.hour
if mood_hour >= 0 && mood_hour < 14
day_part = "Morning"
elsif mood_hour >= 14 && mood_hour < 20
day_part = "Day"
else
day_part = "Evening"
end
# create a cell from a given mood object and calculated part of the day:
section[:cells] << {
date: day_part,
mood: mood.level,
comment: mood.comment
}
end
# add freshly created section to a @data array:
@data << section
end
# update table with updated @data:
@table.reloadData
end
When you launch the app, it should start showing you real data:
Now we know how to create and retrieve objects. But how can we delete something? Realm allows us to delete an object very quickly:
RLMRealm.write do |realm
realm.delete(object)
end
# or:
object.delete
# if you want to remove all objects of a given class:
Mood.delete_all
# if you want to clear the whole DB:
RLMRealm.write do |realm|
realm.delete_all
end
If we want to delete an object, we need to retrieve it first.
Unfortunately,
we don't have any unique parameters in our schema: a user can
accidentally add two mood objects with the same date, and the user will
definitely have lots of objects with the same
level
property.
The best idea is to add an
id
property and make it our primary key. To do so, we will need to
update the schema and run a migration. Let's start with schema first.
We just add a new field here:
#import <Realm/Realm.h>
@interface Mood : RLMObject
@property NSDate *created_at;
@property NSString *comment;
@property NSInteger level;
@property NSInteger id;
@end
@implementation Mood
@end
Seems easy! But now we need to figure out how to generate a unique
id for each new mood. To do it, we can look for a mood object
with a highest ID property, and then increment it.
To set ID on each new object automatically, we will use
Realm's feature that helps us setting default values on each object.
In Mood class, we will add a class method
self.default_values
that will return values that need to be set for each new object. This
method will also call another method written by ourselves, that will
look and return the highest ID:
class Mood
include MotionRealm
def self.default_values
{
"id" => self.next_id
}
end
def self.next_id
# get max ID and increment it:
self.all.max_property("id").to_i + 1
end
end
Now we need to add migration code. Let's add it into
AppDelegate
in a
migrate_db
method:
class AppDelegate < PM::Delegate
def on_load(app, options={})
migrate_db
open_tab_bar HomeScreen.new(nav_bar: true),
MoodHistoryScreen.new(nav_bar: true)
end
def migrate_db
RLMRealmConfiguration.migrate_to_version(1) do |migration, old_version|
migration.enumerate "Mood" do |old_object, new_object|
# update object to 1st schema version:
if old_version < 1
# set object id to an automatically generated:
new_object.id = Mood.next_id
end
end
end
end
# rest of AppDelegate code ...
end
When you open the app, it will migrate to the latest DB schema version. You can test it in REPL:
mood = Mood.first
mood.id
# => some integer value: 1, 2, 3, etc
Now when all mood objects have unique IDs, we will include them into table data. When the user swipes cell to remove a mood object, we will get its ID, and then will find an object in a DB with a given ID, and we will delete it. And don't forget to update table after it:
class MoodHistoryScreen < PM::Screen
# MoodHistoryScreen code ...
def set_table_data
# set_table_data code ...
# add ID to each cell:
section[:cells] << {
date: day_part,
mood: mood.level,
comment: mood.comment,
id: mood.id
}
# rest of set_table_data code ...
end
# use UITableView delegate method to allow swipe-to-delete:
def tableView(table, canEditRowAtIndexPath: index)
# yes, user can edit row at this (and all other) indexes:
true
end
# user has tapped Delete button. Process it now:
def tableView(table, commitEditingStyle: edit_style, forRowAtIndexPath: index)
# there are different edit styles: deletion, updates, etc.
# we want to process deletions only:
if edit_style == UITableViewCellEditingStyleDelete
# get cell at a given index from out @data:
item = @data[index.section][:cells][:index.row]
mood_id = item[:id]
moods = Mood.where "id = #{mood_id}"
# stop processing if we could not find correct Mood object:
return if moods.empty?
mood = moods.first
mood.delete
# regenerate table data:
set_table_data
end
end
tableView(table, commitEditingStyle: edit_style, forRowAtIndexPath: index)
.
Next
we added
tableView(table, canEditRowAtIndexPath: index)
.
This is a
UITableview Delegate
method, and it is used to mark cells that can be edited: it passes a
cell's index, and developer decides whether cell at given index can be
edited. If it can, the method should return
true
,
otherwise -
false
. Since we can edit all our cells, we just return
true
.
After that we added
tableView(table, commitEditingStyle: edit_style, forRowAtIndexPath: index)
.
This delegate method is called when some cell has been edited.
This means
that cell could be moved to a new position or deleted. According to
edit_style
,
we can figure out what to do.
In our case, we are interested in deletions only. We get a cell's data
from
@data
array with
item = @data[index.section][:cells][:index.row]
,
and then we get its ID. If the database does not have any mood objects
with this ID, we just stop processing it. Otherwise, we delete it
and call
set_table_data
method that generates the data for us and updates table with
@table.reloadData
.
We just added Mood Model to the app, let's add new commit to git:
git add .
git commit -m "Mood Model added"
In this chapter,
we tried to use Realm on practice. Created a schema with Objective-C,
added some migrations, and learned how to create and delete objects.
We also learned how to work with some new
UITableView
methods: how to reload a table to show new data, how to allow a user
to edit cells, and how to perform deletions.