Realm Tutorial: Getting Started
In this tutorial, you’ll learn how to use the Realm cross-platform mobile database solution by building an app that keeps track of wild animals. By Felipe Laso-Marsetti.
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
Realm Tutorial: Getting Started
30 mins
Realm is a cross-platform mobile database solution designed for mobile applications that you can integrate with your iOS projects. Unlike wrappers around Core Data, Realm doesn’t rely on Core Data or even an SQLite back end.
This tutorial introduces you to the basic features of Realm on iOS. By the end of this tutorial, you’ll know how to link the Realm framework, create models, perform queries and update records.
Getting Started
Use the Download Materials button at the top or bottom of this tutorial to download the starter project.
Here’s the scenario: You’ve accepted a position as an intern in the National Park Service. Your job is to document the species found in the largest national parks in the United States.
You’ll need an assistant to keep notes of your findings, but since the agency doesn’t have the budget to hire a new one, you decide to create a virtual assistant for yourself: an app named Agents Partner.
Open the starter project in Xcode. Currently, the app contains only the map functionality using MapKit, which is already set up in the project.
The starter project is missing Realm, so it’s time to add it.
An efficient way to install Realm is with CocoaPods.
In the root directory of the starter project, create a new file named Podfile. Copy the following text and paste it into the newly created file:
platform :ios, '12.0'
use_frameworks!
target 'Agents Partner' do
pod 'RealmSwift'
end
Save and close the file.
In Terminal and in the root directory of your project, run the following command:
pod install
This tells CocoaPods to scan through your Podfile and install any pods that you have listed in the file. Pretty neat!
It may take a bit for Realm to install, so keep an eye on your terminal. Once it’s complete, you’ll see a line near the bottom that begins with Pod installation complete!
.
In Finder, open the root directory of the starter project. Notice the folders that CocoaPods added as well as Agents Partner.xcworkspace.
If you have the starter project open in Xcode, close it now and open .xcworkspace by double-clicking the file. This file is what you need to open when you want to work on the project.
You just set up Realm using CocoaPods. Build and run the project to ensure everything compiles. If everything went as expected, you’ll see this:
Concepts and Classes Overview
To better understand what Realm does, here are some concepts and information about the classes you’ll use in this tutorial:
-
Realm: Realm instances are the heart of the framework. It’s your access point to the underlying database like a Core Data managed object context. You create instances using the
Realm()
initializer. -
Object: This is your Realm model. The act of creating a model defines the schema of the database. To create a model, you subclass
Object
and define the fields you want to persist as properties. -
Relationships: You create one-to-many relationships between objects by declaring a property of the type of the
Object
to which you want to refer. You can create many-to-one and many-to-many relationships via a property of typeList
. -
Write Transactions: Any operations in the database, like creating, editing or deleting objects, must be performed within writes by calling
write(_:)
onRealm
instances. -
Queries: To retrieve objects from the database you use queries. The simplest form of a query is calling
objects()
on aRealm
instance and passing in the class of theObject
you’re looking for. If your data retrieval needs are more complex, you can make use of predicates, chain your queries and order your results. -
Results: Results is an auto-updating container type that you get back from object queries. They have many similarities with regular
Arrays
including the subscript syntax.
With a brief introduction to Realm, it’s time to get your feet wet and build the rest of the project.
Your First Model
Open Specimen.swift from the Models group and add the following implementation:
import Foundation
import RealmSwift
class Specimen: Object {
@objc dynamic var name = ""
@objc dynamic var specimenDescription = ""
@objc dynamic var latitude = 0.0
@objc dynamic var longitude = 0.0
@objc dynamic var created = Date()
}
The code above adds a few properties:
name
and specimenDescription
store the specimen’s name and description. Specific data types in Realm, such as strings, must be initialized with a value. In this case, you initialize them with an empty string.
latitude
and longitude
store the coordinates for the specimen. Here, you set the type to Double
and initialize them with 0.0
.
created
stores the creation date of the specimen. Date()
returns the current date so the property is initialized with that value.
With your first model created in Realm, are you ready to use this knowledge in a small challenge?
Specimens should be separated into different categories. The challenge is to create a Category
model by yourself. Name the file Category.swift and give your new model a single String
property named name
.
If you want to check your work, the solution is below:
[spoiler title=”Category object”]
Category.swift will look like this:
import Foundation
import RealmSwift
class Category: Object {
@objc dynamic var name = ""
}
[/spoiler]
You have a Category
model that you somehow need to relate to the Specimen
model.
Recall the note above that stated you could create relationships between models by declaring a property with the appropriate model to be linked.
Open Specimen.swift and add the following declaration below the other properties:
@objc dynamic var category: Category!
This sets up a one-to-many relationship between Specimen
and Category
. This means each Specimen
can belong to only one Category
, but each Category
can have many Specimen
s.
You now have your basic data models in place. It’s time to add some records to your database!
Adding Records
When the user adds a new specimen, they can enter the specimen name and select a category. Open CategoriesTableViewController.swift. This view controller presents the list of categories in a table view so the user can select one.
Before you start writing code to integrate Realm, you need to import the RealmSwift framework. Add the following line to the top of the file, below import UIKit
:
import RealmSwift
You’ll populate this table view with some default categories. These Category
instances can be stored in an instance of Results
.
CategoriesTableViewController
has a categories
array as a placeholder for now. Find the following code at the top of the class definition:
var categories: [Any] = []
Replace that code with the following:
let realm = try! Realm()
lazy var categories: Results<Category> = { self.realm.objects(Category.self) }()
When you want to fetch objects, you always define the models you want. In the code above, you first create a Realm
instance and then populate categories
by calling objects(_:)
on it, passing in the class name of the model type you want.
try!
when calling Realm methods that throw an error. In your own code, you should be using try
and do
/ catch
to catch and handle errors.
You want to give your user some default categories to choose from the first time the app runs.
Add the following helper method to the class definition:
private func populateDefaultCategories() {
if categories.count == 0 { // 1
try! realm.write() { // 2
let defaultCategories =
["Birds", "Mammals", "Flora", "Reptiles", "Arachnids" ] // 3
for category in defaultCategories { // 4
let newCategory = Category()
newCategory.name = category
realm.add(newCategory)
}
}
categories = realm.objects(Category.self) // 5
}
}
Here’s what’s going on in each numbered line:
- If
count
is equal to 0 this means the database has noCategory
records. This is the case the first time you run the app. - This starts a transaction on
realm
, and you’re now ready to add some records to the database. - Here, you create the list of default category names and then iterate through them.
- For each category name, you create a new instance of
Category
, populatename
and add the object torealm
. - You fetch all of the categories you created and store them in
categories
.
Add the following line to the end of viewDidLoad()
:
populateDefaultCategories()
This calls the helper method to populate your test categories when the view loads.
Now that you have some data, you’ll update the table view data source methods to show the categories. Find tableView(_:cellForRowAt:)
and add the following before return cell
:
let category = categories[indexPath.row]
cell.textLabel?.text = category.name
This implementation retrieves a category from categories
based on the index path. It then sets the cell’s text label to show the category’s name
.
Next, add this property below the other properties you added to CategoriesTableViewController
:
var selectedCategory: Category!
You’ll use this property to store the currently selected Category
.
Find tableView(_:willSelectRowAtIndexPath:)
and add the following before return indexPath
:
selectedCategory = categories[indexPath.row]
This stores the user’s selection in the property selectedCategory
you declared above.
Build and run your app.
Zoom and pan the map to somewhere interesting and create a new annotation by tapping on the + button in the top-right. Tap on the map pin to select it, and then tap on the annotation data to edit the details. Finally, tap the Category text field to see the list of categories as shown below:
You can select a category, but that only saves it to the property and not anywhere else in the database. It’s nice to see the categories show up in the app, but it’s always reassuring to see the records in the database. You can do this via the Realm Browser.
Introducing the Realm Browser
Realm includes the Realm Browser for reading and editing databases. The Realm database format is proprietary and not human-readable.
You can download the Realm Browser here.
Working With Realm Browser
It’s important to know where your Realm database is stored while developing your app — and there’s a neat trick you can use to find out where it is.
Open MapViewController.swift and add the following line to the top of the file below the existing import
statements:
import RealmSwift
Add the following line to viewDidLoad()
after the call to super.viewDidLoad()
:
print(Realm.Configuration.defaultConfiguration.fileURL!)
This line prints the database location to the debug console. It’s a short step to then browse the database using the Realm Browser.
Build and run your app, and you’ll see that it reports the location of the database in the Xcode console.
The easiest way to go to the database location is to open Finder, press Shift-Command-G and paste in the path your app reported.
Once you open the folder in Finder, you might see one or two files. One of them is default.realm, which is your database file. The second file, that may or may not be present, is default.realm.lock. The lock file prevents modification from other apps while the database is in use.
If you haven’t yet downloaded the Realm Browser, download it from the App Store. Double-click default.realm to open it with Realm Browser:
Once the database is open in Realm Browser, you’ll see Category with a 5 next to it. This means that this class contains five records. Click a class to inspect the individual fields contained within.
Adding Categories
You can now put in place the logic to set the category
of a Specimen
.
Open AddNewEntryController.swift and import the RealmSwift framework below the existing import statements:
import RealmSwift
Add the following property to the class:
var selectedCategory: Category!
You’ll use this to store the selected Category
.
Next, find unwindFromCategories(segue:)
and add the following implementation inside:
if segue.identifier == "CategorySelectedSegue" {
let categoriesController = segue.source as! CategoriesTableViewController
selectedCategory = categoriesController.selectedCategory
categoryTextField.text = selectedCategory.name
}
unwindFromCategories(segue:)
is called when the user selects a category from CategoriesTableViewController
that you set up in the previous step. Here, you retrieve the selected category, store it in selectedCategory
and fill in the text field with the category’s name.
You can continue by creating your first Specimen
!
Adding Specimens
Still in AddNewEntryController.swift, add one more property to the class:
var specimen: Specimen!
This property stores the new specimen object.
Next, add this helper method to the class:
func addNewSpecimen() {
let realm = try! Realm() // 1
try! realm.write { // 2
let newSpecimen = Specimen() // 3
newSpecimen.name = nameTextField.text! // 4
newSpecimen.category = selectedCategory
newSpecimen.specimenDescription = descriptionTextField.text
newSpecimen.latitude = selectedAnnotation.coordinate.latitude
newSpecimen.longitude = selectedAnnotation.coordinate.longitude
realm.add(newSpecimen) // 5
specimen = newSpecimen // 6
}
}
Here’s what the code above does:
- First, get a
Realm
instance, like before. - Start the write transaction to add your new
Specimen
. - Create a new
Specimen
instance. - Assign the
Specimen
values. The values come from the input text fields in the user interface, the selected categories and the coordinates from the map annotation. - Add the new
Specimen
to the realm. - Assign the new
Specimen
to yourspecimen
property.
You’ll need some sort of validator to make sure all of the fields are populated in your Specimen
. validateFields()
in AddNewEntryViewController
exists to check for a specimen name and description. Since you’ve added the ability to assign a category to a specimen, you’ll check for that field too.
Find the line in validateFields()
that looks like this:
if nameTextField.text!.isEmpty || descriptionTextField.text!.isEmpty {
Change that line to this:
if
nameTextField.text!.isEmpty ||
descriptionTextField.text!.isEmpty ||
selectedCategory == nil {
This verifies that all of the fields are populated and that you’ve selected a category.
Next, add the following method to the class:
override func shouldPerformSegue(
withIdentifier identifier: String,
sender: Any?
) -> Bool {
if validateFields() {
addNewSpecimen()
return true
} else {
return false
}
}
In the code above, you call the method to validate the fields. If everything is filled in, you add the new specimen and return `true`; otherwise, you return `false`.
Build and run. Tap the + button to create a new specimen. Fill in the name and description, select a category, and then tap Confirm to add your Specimen
to the database.
The view controller dismisses, but nothing appears to happen. What’s the deal?
You posted the record to your realm, but you haven’t populated the map with your new specimen.
Retrieving Records
You added a specimen to the database that you want to show on the map.
Start by taking another look at the updated database in the Realm Browser:
You’ll see a single specimen with its fields populated, along with the latitude and longitude from the MKAnnotation
. You’ll also see the link to your specimen’s category; this means your one-to-many Category
relationship is working as expected.
Click the Category
in your Specimen
record to view the Category
record itself.
Next, you’ll populate the map in the app.
Open SpecimenAnnotation.swift and add a property to the class:
var specimen: Specimen?
This holds the Specimen
for the annotation.
Next, replace the initializer with the following:
init(
coordinate: CLLocationCoordinate2D,
title: String,
subtitle: String,
specimen: Specimen? = nil
) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.specimen = specimen
}
The change here is to add an option to pass in a Specimen
. The specimen has a default value of nil
meaning you can omit that argument if you like. The rest of the app can still call the initializer with the first three arguments if there’s no specimen.
Open MapViewController.swift and add a new property to the class:
var specimens = try! Realm().objects(Specimen.self)
Since you want to store a collection of specimens in this property, you ask a Realm
instance for all objects of type Specimen
.
Now, add the following method to the class:
func populateMap() {
mapView.removeAnnotations(mapView.annotations) // 1
specimens = try! Realm().objects(Specimen.self) // 2
// Create annotations for each one
for specimen in specimens { // 3
let coord = CLLocationCoordinate2D(
latitude: specimen.latitude,
longitude: specimen.longitude);
let specimenAnnotation = SpecimenAnnotation(
coordinate: coord,
title: specimen.name,
subtitle: specimen.category.name,
specimen: specimen)
mapView.addAnnotation(specimenAnnotation) // 4
}
}
Reviewing each numbered comment, you:
- Clear out all the existing annotations on the map to start fresh.
- Refresh your
specimens
property. - Loop through
specimens
and create aSpecimenAnnotation
with the coordinates of the specimen, as well as itsname
andcategory
. - Add each
specimenAnnotation
to theMKMapView
.
You need to call this method from somewhere. Find viewDidLoad()
and add this line to the end of its implementation:
populateMap()
That ensures that the map shows the specimens when the view controller loads.
Now you’ll change your annotation to include the specimen name and category. Find unwindFromAddNewEntry(segue:)
and replace the method with the following implementation:
@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) {
let addNewEntryController = segue.source as! AddNewEntryViewController
let addedSpecimen = addNewEntryController.specimen!
let addedSpecimenCoordinate = CLLocationCoordinate2D(
latitude: addedSpecimen.latitude,
longitude: addedSpecimen.longitude)
if let lastAnnotation = lastAnnotation {
mapView.removeAnnotation(lastAnnotation)
} else {
for annotation in mapView.annotations {
if let currentAnnotation = annotation as? SpecimenAnnotation {
if currentAnnotation.coordinate.latitude == addedSpecimenCoordinate.latitude &&
currentAnnotation.coordinate.longitude == addedSpecimenCoordinate.longitude {
mapView.removeAnnotation(currentAnnotation)
break
}
}
}
}
let annotation = SpecimenAnnotation(
coordinate: addedSpecimenCoordinate,
title: addedSpecimen.name,
subtitle: addedSpecimen.category.name,
specimen: addedSpecimen)
mapView.addAnnotation(annotation)
lastAnnotation = nil;
}
The system calls this method once you’ve returned from AddNewEntryController
and there’s a new specimen to add to the map. When you add a new specimen to the map, it gets the generic annotation icon. With your category, you want to change that icon to the category-specific icon.
Here, you remove the last annotation added to the map and replace it with one that shows the specimen’s name and category.
Build and run. Create some new specimens of different categories and see how the map updates:
A Different View
You might have noticed the Log button in the top-left of the map view. In addition to the map, the app also has a text-based table view listing of all annotations called the Log View. Next, you’ll populate this table view with some data.
Open LogViewController.swift and import RealmSwift
:
import RealmSwift
Then, replace the specimens
property with the following:
var specimens = try! Realm().objects(Specimen.self)
.sorted(byKeyPath: "name", ascending: true)
In the code above, you replace the placeholder array with Results
that holds Specimen
s as you did in MapViewController
. They’ll be sorted by name
.
Next, add the following to tableView(_:cellForRowAt:)
before return cell
:
let specimen = specimens[indexPath.row]
cell.titleLabel.text = specimen.name
cell.subtitleLabel.text = specimen.category.name
switch specimen.category.name {
case "Uncategorized":
cell.iconImageView.image = UIImage(named: "IconUncategorized")
case "Reptiles":
cell.iconImageView.image = UIImage(named: "IconReptile")
case "Flora":
cell.iconImageView.image = UIImage(named: "IconFlora")
case "Birds":
cell.iconImageView.image = UIImage(named: "IconBird")
case "Arachnid":
cell.iconImageView.image = UIImage(named: "IconArachnid")
case "Mammals":
cell.iconImageView.image = UIImage(named: "IconMammal")
default:
cell.iconImageView.image = UIImage(named: "IconUncategorized")
}
This method populates the cell with the specimen’s name and category.
Build and run your app. Tap Log and you’ll see all of your entered specimens in the table view like so:
Fetches With Predicates
You want your app to rock, so you’ll need a handy search feature. Your starter project contains an instance of UISearchController
; You’ll add a few modifications specific to your app to make it work with Realm.
In LogViewController.swift, replace the searchResults
property with the following:
var searchResults = try! Realm().objects(Specimen.self)
Add this method to the class:
func filterResultsWithSearchString(searchString: String) {
let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString) // 1
let scopeIndex = searchController.searchBar.selectedScopeButtonIndex // 2
let realm = try! Realm()
switch scopeIndex {
case 0:
searchResults = realm.objects(Specimen.self)
.filter(predicate).sorted(byKeyPath: "name", ascending: true) // 3
case 1:
searchResults = realm.objects(Specimen.self).filter(predicate)
.sorted(byKeyPath: "created", ascending: true) // 4
default:
searchResults = realm.objects(Specimen.self).filter(predicate) // 5
}
}
Here’s what the above function does:
- First, you create a predicate which searches for
name
s that start withsearchString
. The[c]
that followsBEGINSWITH
indicates a case insensitive search. - You then grab a reference to the currently selected scope index from the search bar.
- If the first segmented button is selected, sort the results by name ascending.
- If the second button is selected, sort the results by created date ascending.
- If none of the buttons are selected, don’t sort the results, take them in the order they’re returned from the database.
Now, you’ll actually perform the filtering when the user interacts with the search field. In updateSearchResults(for:)
, add the following two lines at the beginning of the method:
let searchString = searchController.searchBar.text!
filterResultsWithSearchString(searchString: searchString)
Since the search results table view calls the same data source methods, you need to make a small change to tableView(_:cellForRowAt:)
to handle both the main log table view and the search results. In that method, find the line that assigns to specimen
:
let specimen = specimens[indexPath.row]
Delete it and replace it with the following:
let specimen = searchController.isActive ?
searchResults[indexPath.row] : specimens[indexPath.row]
This code checks whether the searchController
is active. If so, it retrieves the specimen from searchResults
. If not, it retrieves the specimen from specimens
instead.
Next, you’ll add a function to sort the returned results when the user taps a button in the scope bar.
Add the following implementation to scopeChanged(sender:)
:
let scopeBar = sender as! UISegmentedControl
let realm = try! Realm()
switch scopeBar.selectedSegmentIndex {
case 1:
specimens = realm.objects(Specimen.self)
.sorted(byKeyPath: "created", ascending: true)
default:
specimens = realm.objects(Specimen.self)
.sorted(byKeyPath: "name", ascending: true)
}
tableView.reloadData()
Here, you check which scope button is pressed (A-Z, or Date Added) and sort. By default, the list will sort by name
.
Build and run your app. Try a few different searches and see what you get for results.
Updating Records
You’ve covered adding records, but what about when you want to update them?
If you tap in a cell in LogViewController
, you’ll segue to AddNewEntryViewController
, but with the fields empty. The first step to letting the user edit the fields is to show the existing data.
Open AddNewEntryViewController.swift and add the following helper method to the class:
func fillTextFields() {
nameTextField.text = specimen.name
categoryTextField.text = specimen.category.name
descriptionTextField.text = specimen.specimenDescription
selectedCategory = specimen.category
}
This method fills in the user interface with the specimen data. Remember, AddNewEntryViewController
has, up to this point, only been used for new specimens, so those fields have always started empty.
Next, add the following lines to the end of viewDidLoad()
:
if let specimen = specimen {
title = "Edit \(specimen.name)"
fillTextFields()
} else {
title = "Add New Specimen"
}
This code sets the navigation title to indicate whether the user is adding or updating a specimen. If it’s an existing specimen, you also call your helper method to populate the fields.
You need a method to update the specimen record with the user’s changes. Add the following method:
func updateSpecimen() {
let realm = try! Realm()
try! realm.write {
specimen.name = nameTextField.text!
specimen.category = selectedCategory
specimen.specimenDescription = descriptionTextField.text
}
}
As usual, the method begins with getting a Realm
instance and then the rest is wrapped inside a write()
transaction. Inside the transaction you update the data fields.
Six lines of code to update the Specimen
record is all it takes! :]
Next, you’ll call the above method when the user taps Confirm. Find shouldPerformSegue(withIdentifier:sender:)
and replace the call to addNewSpecimen()
with the following:
if specimen != nil {
updateSpecimen()
} else {
addNewSpecimen()
}
This calls your method to update the data when appropriate.
Open LogViewController.swift and add the following implementation for prepare(for:sender:)
:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "Edit") {
let controller = segue.destination as! AddNewEntryViewController
var selectedSpecimen: Specimen!
let indexPath = tableView.indexPathForSelectedRow
if searchController.isActive {
let searchResultsController =
searchController.searchResultsController as! UITableViewController
let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow
selectedSpecimen = searchResults[indexPathSearch!.row]
} else {
selectedSpecimen = specimens[indexPath!.row]
}
controller.specimen = selectedSpecimen
}
}
You’ll pass the selected specimen to the AddNewEntryController
instance. The complication with the if/else
is because getting the selected specimen is different depending on whether or not the user is looking at search results.
Build and run your app. Open the Log view and tap on a Specimen
. You’ll see the details with all of the fields populated and ready for editing.
Where to Go From Here?
You can download the final project using the Download materials button at the top or bottom of this tutorial.
In this tutorial, you learned how to create, update and fetch records from a Realm database. You also learned how to use predicates and sort the results by their properties.
Many features of Realm were not covered in this tutorial, but you can learn about those topics and much more in the official documentation or with our book, Realm: Building Modern Swift Apps with Realm Database.
Thank you for taking the time to read this tutorial. I hope you enjoyed it :)
If you have any comments or questions on this tutorial, please join the discussion below!