Working with Realm

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:

When Realm is compiled, copy 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 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:

To create schema like this we will need to add next code to the 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

Mood Table

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:

Deleting Objects

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
        
      
As you can see, first we added mood's ID into each cell. Later we will retrieve it, and will use it to find a selected mood in a 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.

Version Control

We just added Mood Model to the app, let's add new commit to git:

        git add .
        git commit -m "Mood Model added"
      

Summary

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.

Book Index | Next