All videos. All books. One low price.

Get unlimited access to all video courses and books on this site with the new raywenderlich.com Ultimate Subscription. Plans start at just $19.99/month.

Home iOS & Swift Tutorials

Realm with SwiftUI Tutorial: Getting Started

In this SwiftUI Realm tutorial, you’ll learn how to use Realm with SwiftUI as a data persistence solution by building a potion shopping list app.

5/5 3 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

Realm Mobile Database is a popular object database management system. It’s open-source, and it can be used on multiple platforms. Realm aims to be a fast, performant, flexible and simple solution for persisting data, while still writing type-safe, Swift code.

SwiftUI is Apple’s latest and hottest UI framework. It uses a declarative syntax to build your views using Swift code. It relies on states to reactively update its views when the user interacts with it. Since Realm uses Live Objects that also update automatically, mixing both frameworks just makes sense!

In this SwiftUI Realm tutorial, you’ll learn how to:

  • Set up Realm
  • Define data models
  • Perform basic CRUD operations on objects
  • Propagate changes from the database to the UI
  • Handle migrations when your data model changes

You’ll learn all this by implementing a Realm database in an app that keeps track of all the ingredients you need for making magic potions! So grab your potions kit, because it’s time to dive right into this cauldron. :]

Note: This SwiftUI Realm tutorial assumes you are already familiar with SwiftUI. If you’re just getting started with SwiftUI, check out our SwiftUI video course or the SwiftUI by Tutorials book.

Getting Started

Download the project materials using the Download Materials button at the top or bottom of this tutorial. Open PotionsMaster.xcodeproj inside the starter folder.

PotionsMaster is an app built for all your potion-making needs. Potion brewing is a challenging skill. Stirring techniques, timing, and bottling can be arduous tasks, even for experienced wizards. This app helps you keep track of the ingredients you need and those you’ve already bought. With PotionsMaster, even that difficult new potion you’ve been reading about will be a snap!

Now it’s time to get to work. To start, build and run.

SwiftUI Realm: List of ingredients and bought ingredients

PotionsMaster is a simple app that lets you add, update and delete ingredients in a list. But as you play around with the app, you’ll notice a small problem. No matter what you do, the app doesn’t persist your data! In fact, it doesn’t perform any actions when you try to create, update or delete an ingredient. But don’t worry — you’re about to make it work, using Realm.

Project Structure

Before you dive in and fix the app so you can start brewing your next potion, take a close look at the starter project. It contains the following key files:

  • Ingredient.swift: This struct is a representation of an ingredient.
  • IngredientStore.swift: This is a class implementation of the Store Pattern. This class handles storing ingredients. It’s also where you’ll perform actions on those ingredients.
  • IngredientListView.swift: This is the main view of the app. This class displays a List. There’s one Section for ingredients to buy, and another for bought ingredients.
  • IngredientFormView.swift: You’ll use this Form to create and update ingredients.

The project uses the Store Pattern to handle states and propagate changes to the UI. IngredientStore has a list of ingredients, those you need to buy and those you’ve already bought. When the user interacts with the app, an action mutates the state. Then SwiftUI receives a signal to update the UI with the new state.

Working with Realm

Realm was built to address common problems of today’s apps. It provides many elegant, easy-to-use solutions. And it’s available for multiple platforms, including but not limited to:

  • Swift/Objective-C
  • Java/Kotlin
  • JavaScript
  • .NET

The coolest part about Realm is that the skills are transferable. Once you learn the Realm basics in one language, they’re easy to pick up in another language. And because Realm is a cross-platform database, its APIs don’t change much from one language to the next.

Still, Realm is not an all-purpose database. Unlike SQLite, Realm is a NoSQL object database. Like any other NoSQL database, it has advantages and disadvantages. But Realm is a great alternative for keeping multi-platform teams in sync.

Understanding the Realm Database

Before you start setting up Realm, you need to understand a bit about how it works.

Realm uses files to save and manage your database. Each Realm database in your app is called a realm. Your app may have multiple realms, each handling a different domain of objects. That helps keep your database organized and concise in your app. And because it’s available across platforms, you can share pre-loaded Realm files between platforms like iOS and Android. That’s really helpful, right? :]

To open a Realm file, you simply instantiate a new Realm object. If you don’t pass a custom file path, Realm creates a default.realm file in the Documents folder on iOS.

Sometimes you might want to use a realm without actually writing data on disk. The database provides a handy solution for these situations: in-memory realms. These can be useful when writing unit tests. You can use in-memory realms to pre-load, modify and delete data for each test case without actually writing on disk.

Realm Mobile Database isn’t the only product Realm provides. The company also offers Realm Sync, a solution for synchronizing Realm databases across multiple devices and in the cloud. Additionally, Realm provides a great app to open, edit and manage your databases: Realm Studio.

Setting up Realm

To start using Realm, you must include it as a dependency in your project. There are many dependency management tools you can use for this, such as the Swift Package Manager and CocoaPods. This tutorial uses Swift Package Manager, but feel free to use the tool you’re more comfortable with.

Note: If you’re not familiar with Swift Package Manager, or if you want to learn more about it, check out our Swift Package Manager for iOS tutorial.

To set up your dependency, select File ▸ Swift Packages ▸ Add Package Dependency…. Copy the following and paste into the combined search/input box:

https://github.com/realm/realm-cocoa.git

This is the location of the GitHub repository containing the Realm package. Click Next.

Xcode window with a search/input to type the dependency location

Next, Xcode asks you to define some options for this package. Simply leave the default value of Up to Next Major to use the latest version of Realm. (As of writing this is therefore from 5.4.0, inclusive, to 6.0.0, exclusive.) Click Next.

Xcode window with options of the package version, branch and commit

Finally, select the package products and targets it should add to the project. Select both, Realm and RealmSwift, and click Finish. Xcode downloads the code from the repository and adds the Realm package to the PotionsMasrer target.

Xcode window with a list of targets to select

Build and run to be sure everything is working.

List of ingredients and bought ingredients

Now that you have Realm set up, you’re ready to create your first data model.

Defining Your Realm Data Model

To build your ingredient model, add a new Swift file in the Models group and name it IngredientDB.swift. Add the following code:

import RealmSwift
// 1
class IngredientDB: Object {
  // 2
  @objc dynamic var id = 0
  @objc dynamic var title = ""
  @objc dynamic var notes = ""
  @objc dynamic var quantity = 1
  @objc dynamic var bought = false

  // 3
  override static func primaryKey() -> String? {
    "id"
  }
}

Here’s a breakdown of the code you just added:

  1. First, you define your class, subclassing Object, Realm’s base class for all data models. You’ll use this class to save your ingredients in Realm.
  2. Here, you define each Ingredient property that you want Realm to store.
  3. Finally, you override primaryKey() to tell Realm which property is the model’s primary key. Realm uses primary keys to enforce uniqueness. A primary key also provides an efficient way to fetch and update data.

That’s it!

You define Realm data models as regular Swift classes. Realm uses these classes to write your data on disk, but there are some restrictions specific to Object subclasses. Because of it’s cross-platform nature, Realm only supports a limited set of platform-independent property types. Some of those properties are:

  • Bool
  • Int
  • Double
  • Float
  • String
  • Date
  • Data

Optional properties have special restrictions. You may declare String, Date, and Data as optional. For the rest, you use the wrapper class RealmOptional; otherwise, they must have a value.

Each property has the @objc dynamic keywords. This makes them accessible at runtime via Dynamic Dispatch. Realm uses this Swift and Objective-C feature to create a facade between reading and writing data. When accessing a property, Swift delegates to Realm the responsibility of providing you with the desired data.

Defining Relationships

Realm also supports relationships. You can declare nested objects to create many-to-one relationships. And you can use List to create many-to-many relationships. When declaring a List, Realm saves those nested objects together with your data model.

Adding an Initializer

Before moving on, open Ingredient.swift and add the following extension at the bottom of the file:

// MARK: Convenience init
extension Ingredient {
  init(ingredientDB: IngredientDB) {
    id = ingredientDB.id
    title = ingredientDB.title
    notes = ingredientDB.notes
    bought = ingredientDB.bought
    quantity = ingredientDB.quantity
  }
}

This extension creates a convenience initializer for mapping IngredientDB to Ingredient. You’ll use this later in the project.

Now that you’ve defined your data model, it’s time to add an object to your database.

Adding Objects to the Database

In the ViewModels group, open IngredientStore.swift. Import RealmSwift by adding the following line at the top of the file:

import RealmSwift

Next, replace the body of create(title:notes:quantity:) with the following code:

objectWillChange.send()

First, you send a signal to SwiftUI. Because IngredientStore is an ObservableObject, SwiftUI subscribes to objectWillChange and responds to the signal by reloading its view.

Next, add the following code to the method:

do {
  let realm = try Realm()

  let ingredientDB = IngredientDB()
  ingredientDB.id = UUID().hashValue
  ingredientDB.title = title
  ingredientDB.notes = notes
  ingredientDB.quantity = quantity
} catch let error {
  // Handle error
  print(error.localizedDescription)
}

First, create an instance of Realm by opening the default realm. With it, you can write, read, update and delete objects. Next, create an IngredientDB object and set its property values from the method’s parameters.

You can instantiate and use Realm data models like any other Swift object. You call those unmanaged objects. That means the database doesn’t know about them yet, and any changes won’t persist. Once you add an object to a realm, it becomes managed by Realm. That means Realm stores the object on disk and keeps tracks of its changes.

Add the model to the realm by adding these few lines to code to the end of the do block:

try realm.write {
  realm.add(ingredientDB)
}

You start a write transaction by calling write on the Realm. Each operation you make on a realm must be inside this write transaction block, including additions, deletions and updates. Inside the transaction, you add the new instance of IngredientDB to Realm. Realm is now storing the object and tracking its changes, making it a managed object.

Build and run, then go ahead and create an ingredient! Tap New Ingredient, give it a title and tap Save.

Ingredient form with title quantity and notes

But wait, something’s still amiss. You create an ingredient, but nothing happens! It still lists the same ingredients as before.

That’s because IngredientStore does not fetch ingredients from Realm yet — it’s still using the mock data. You’ll fix this next.

Fetching Objects

Open IngredientStore.swift again, and locate the following:

var ingredients: [Ingredient] = IngredientMock.ingredientsMock
var boughtIngredients: [Ingredient] = IngredientMock.boughtIngredientsMock

Replace that code with this:

private var ingredientResults: Results<IngredientDB>
private var boughtIngredientResults: Results<IngredientDB>

When fetching objects from Realm, the database returns a Results type. This type represents a collection of objects retrieved from queries.

Now add the following initializer below the two lines you just added:

// 1
init(realm: Realm) {
  // 2
  ingredientResults = realm.objects(IngredientDB.self)
    .filter("bought = false")
  // 3
  boughtIngredientResults = realm.objects(IngredientDB.self)
    .filter("bought = true")
}

Here’s what’s going on in the above initializer:

  1. First, you receive an instance of Realm. You’ll use this instance to fetch ingredients.
  2. Next, you fetch ingredients from realm and filter them with bought as false.
  3. Then you fetch ingredients from realm and filter them with bought as true.

Finally, insert the following code after the initializer:

var ingredients: [Ingredient] {
  ingredientResults.map(Ingredient.init)
}

var boughtIngredients: [Ingredient] {
  boughtIngredientResults.map(Ingredient.init)
}

These properties turn Realm’s Result into a regular array. The sample project’s UI uses these computed properties to map the database models to views.

Since IngredientStore now requires a Realm in its initializer, you need to provide it. Open ScenceDelegate.swift. After the import SwiftUI statement, import RealmSwift by inserting the following:

import RealmSwift

Next, change the code inside scene(_:willConnectTo:options:) to this:

if let windowScene = scene as? UIWindowScene {
  do {
    // 1
    let realm = try Realm()
    let window = UIWindow(windowScene: windowScene)
    // 2
    let contentView = ContentView()
      .environmentObject(IngredientStore(realm: realm))
    window.rootViewController = UIHostingController(rootView: contentView)
    self.window = window
    window.makeKeyAndVisible()
  } catch let error {
    // Handle error
    fatalError("Failed to open Realm. Error: \(error.localizedDescription)")
  }
}

Here’s what you’re doing:

  1. You create a new instance of Realm.
  2. You instantiate IngredientStore with the Realm instance, and you add it to the ContentViews environment.

Build and run. Now create an ingredient and see the magic!

Ingredient form with title quantity and notes

Realm works with Live Objects. When you add an ingredient to Realm, ingredientResults updates automatically without you needed to fetch it each time. SwiftUI receives a signal to update the UI with a new, up-to-date view. Feels like magic, right? Go ahead and create some more ingredients! :]

Now that you can successfully add an ingredient, it’s time to build the functionality to update existing ingredients.

Updating Objects

A key feature of PotionsMaster is the ability to toggle an ingredient to the BOUGHT list. Right now, if you tap the buy button, nothing happens. To fix this, you use Realm to update ingredients on disk.

Toggling Ingredients to BOUGHT

To move an ingredient to the BOUGHT list, you need to update the property value of bought to true on disk.

Open IngredientStore.swift and replace the contents of toggleBought(ingredient:) with the following:

// 1
objectWillChange.send()
do {
  // 2
  let realm = try Realm()
  try realm.write {
    // 3
    realm.create(
      IngredientDB.self,
      value: ["id": ingredient.id, "bought": !ingredient.bought],
      update: .modified)
  }
} catch let error {
  // Handle error
  print(error.localizedDescription)
}

Here’s what’s happening in this code:

  1. You send a signal to SwiftUI indicating that the object is about to change.
  2. You open the default realm.
  3. You start a new write transaction and call create(_:value:update:), passing the updated values and the .modified case. This tells Realm to update the database with the values inside the dictionary. When building the dictionary, you must include the object’s id. If an object with that id already exists, Realm updates it with the new values. Otherwise, Realm creates a new object on disk.

Build and run. Now buy an ingredient by tapping the circular icon on the right of its cell.

List of ingredients. Cursor tapping on the buy button and moving the ingredient to the bought section.

When updating bought, Realm notifies both ingredientResults and boughtIngredientResults of this change. The updated ingredient moves to boughtIngredientResults. And SwiftUI animates the change on the List! How cool is that? :]

Another way of updating an object is by setting its properties to new values. Again, you do this inside a write transaction. Realm will then update each value on disk.

Updating Other Properties

Now that you know how to update an object, you can use Realm to update other properties just as easily. Change the body of update(ingredientID:title:notes:quantity:) to the code below:

// 1
objectWillChange.send()
do {
  // 2
  let realm = try Realm()
  try realm.write {
    // 3
    realm.create(
      IngredientDB.self,
      value: [
        "id": ingredientID,
        "title": title,
        "notes": notes,
        "quantity": quantity
      ],
      update: .modified)
  }
} catch let error {
  // Handle error
  print(error.localizedDescription)
}

Here’s what’s happening in this code:

  1. Once again, you use objectWillChange to send a signal telling SwiftUI to reload the UI.
  2. You open the default realm.
  3. You call create(_:value:update:) inside a write transaction. This call updates the values of the ingredient.

This is similar to the code you added above for buying an ingredient. You call create(_:value:update:), passing the updated values. Realm updates the values on disk and notifies ingredientResults of the changes. Then, SwiftUI updates the UI with those changes.

Build and run. Tap an ingredient’s name to open the form again. Edit some fields and tap Update.

List of ingredients and bought ingredients. Cursor tapping an ingredient opening the form with it and updating its title.

Now, all that’s left is deleting ingredients!

Deleting Objects

Tapping the buy button moves an ingredient to the BOUGHT section. But once it’s there you can’t get rid of it. Tapping the trash can icon does nothing.

To fix this, open IngredientStore.swift. Replace the body of delete(ingredientID:) with the following code:

// 1
objectWillChange.send()
// 2
guard let ingredientDB = boughtIngredientResults.first(
  where: { $0.id == ingredientID }) 
  else { return }

do {
  // 3
  let realm = try Realm()
  try realm.write {
    // 4
    realm.delete(ingredientDB)
  }
} catch let error {
  // Handle error
  print(error.localizedDescription)
}

Here’s how you delete an ingredient in this code:

  1. Again, you use objectWillChange to send a signal requesting SwiftUI to reload the UI.
  2. You find the ingredient to delete from boughtIngredientResults.
  3. You open the default realm.
  4. Finally, you call delete, passing the object you want to delete.

Build and run. Buy an ingredient, and then tap the delete button to remove it from the list.

List of ingredients a bought ingredients. Cursor tapping the buy button moving an ingredient to the bought section, then tapping the delete button on both bought sections.

Adding a New Property to a Realm Object

During development, it’s common for data models to grow and evolve. Property types may change, and you may need to add or remove properties. With Realm, changing your data model is as easy as changing any other Swift class.

In this section, you’ll add a new property to identify your ingredients by color.

Open IngredientDB.swift and add a new property under bought:

@objc dynamic var colorName = "rw-green"

Next, in Ingredient.swift, add the following property:

var colorName = "rw-green"

You also need to update the initializer to set colorName. Add the following line at the bottom of the initializer in the file:

colorName = ingredientDB.colorName

In the three lines of code above, you’ve added a property for storing the color name on Realm and for mapping it in the views.

That’s it! The models are ready to store a color name. Next, you’ll update IngredientStore.swift to save this new property in your database.

Storing the New Property in Realm

Open IngredientStore.swift, and locate this code:

func create(title: String, notes: String, quantity: Int) {

Replace it with this:

func create(title: String, notes: String, quantity: Int, colorName: String) {

Now, insert the following line after where you’ve set other properties like the quantity and notes:

ingredientDB.colorName = colorName

This adds a new parameter, colorName, and assigns it to ingredientDB.

Still in IngredientStore.swift, find this line:

func update(ingredientID: Int, title: String, notes: String, quantity: Int) {

Change it to this:

func update(
  ingredientID: Int,
  title: String,
  notes: String,
  quantity: Int,
  colorName: String
) {

Finally, in update, find this code:

realm.create(
  IngredientDB.self,
  value: [
    "id": ingredientID,
    "title": title,
    "notes": notes,
    "quantity": quantity
  ], 
  update: .modified)

Replace it with the following:

realm.create(
  IngredientDB.self,
  value: [
    "id": ingredientID,
    "title": title,
    "notes": notes,
    "quantity": quantity,
    "colorName": colorName
  ],
  update: .modified)

This code adds a parameter, colorName, to the update. It adds it to the values dictionary when calling create(_:value:update:).

Now both the create and the update methods require a colorName.

But Xcode recognizes that IngredientFormView does not pass a colorName when it calls these methods and produces a couple of errors.

To fix this, open IngredientForm.swift. Add the following code just after the property quantity:

@Published var color = ColorOptions.rayGreen

Now find init(_:ingredient:) and add the following line at the bottom:

color = ColorOptions(rawValue: ingredient.colorName) ?? .rayGreen

Here you add a property to store the color when the user is creating or updating an ingredient.

Next, open IngredientFormView.swift and find the following code inside saveIngredient():

store.create(
  title: form.title,
  notes: form.notes,
  quantity: form.quantity)

Replace it with this:

store.create(
  title: form.title,
  notes: form.notes,
  quantity: form.quantity,
  colorName: form.color.name)

In updateIngredient(), you need to pass colorName in the call to update. To do this, find the following code:

store.update(
  ingredientID: ingredientID,
  title: form.title,
  notes: form.notes,
  quantity: form.quantity)

Replace it with this:

store.update(
  ingredientID: ingredientID,
  title: form.title,
  notes: form.notes,
  quantity: form.quantity,
  colorName: form.color.name)

You’ve now fixed the problem of IngredientFormView not passing colorName to IngredientStore.

Build and run. And you get…

Message on terminal saying a property has been added and a migration is required.

The app crashes! Realm throws a migration error: Migration is required due to the following errors:. But why is this happening?

Working With Migrations

When your app launches, Realm scans your code for classes with Object subclasses. When it finds one, it creates a schema for mapping the model to the database.

When you change a data model, there’s a mismatch between the new schema and the one in the database. If that happens, Realm throws an error. You have to tell Realm how to migrate the old schema to the new one. Otherwise, it doesn’t know how to map old objects to the new schema.

Since you added a new property, colorName, to IngredientDB, you must create a migration for it.

Note: You can solve this during development by passing true to deleteRealmIfMigrationNeeded when you instantiate Realm.Configuration. That tells Realm that, if it needs to migrate, it should delete its file and create a new one.

Creating a Migration

In the Models group, create a file named RealmMigrator.swift.

Now, add this code to your new file:

import RealmSwift

enum RealmMigrator {
  // 1
  static private func migrationBlock(
    migration: Migration,
    oldSchemaVersion: UInt64
  ) {
    // 2
    if oldSchemaVersion < 1 {
      // 3
      migration
        .enumerateObjects(ofType: IngredientDB.className()) { _, newObject in
          newObject?["colorName"] = "rw-green"
        }
    }
  }
}

Here's the breakdown:

  1. You define a migration method. The method receives a migration object and oldSchemaVersion.
  2. You check the version of the file-persisted schema to decide which migration to run. Each schema has a version number, starting from zero. In this case, if the old schema is the first one (before you added a new property), run the migration.
  3. Finally, for each of the old and new IngredientDB objects in Realm, you assign a default value to the new property.

Realm uses migrationBlock to run the migration and update any necessary properties.

At the bottom of RealmMigrator, add the following new static method:

static func setDefaultConfiguration() {
  // 1
  let config = Realm.Configuration(
    schemaVersion: 1,
    migrationBlock: migrationBlock)
  // 2
  Realm.Configuration.defaultConfiguration = config
}

Here's what you're doing in this code:

  1. You create a new instance of Realm.Configuration using your migrationBlock, and set the current version of the schema to 1.
  2. You set the new default configuration of Realm.

Finally, in SceneDelegate.swift, call this new method at the top of scene(_:willConnectTo:options:):

RealmMigrator.setDefaultConfiguration()

Realm uses this configuration to open the default database. When that happens, Realm detects a mismatch between the file-persisted schema and the new schema. It then migrates the changes by running the migration function you just created.

Now build and run again. This time the crash is gone!

List of ingredients and bought ingredients.

You've successfully added a new property to IngredientDB. And you've taken care of the migration. Now it's time to update the form so the user can choose the color!

Adding a New Field to the Form

Open IngredientFormView.swift and find the comment // TODO: Insert Picker here. Insert this code below the comment line:

Picker(selection: $form.color, label: Text("Color")) {
  ForEach(colorOptions, id: \.self) { option in
    Text(option.title)
  }
}

This adds a new picker view to IngredientFormView. This picker lets the user choose a color.

Next, open IngredientRow.swift and find the comment // TODO: Insert Circle view here. Add the following code after the comment:

Circle()
  .fill(Color(ingredient.colorName))
  .frame(width: 12, height: 12)

Here you're adding a circle view to each ingredient row. You fill the circle with the color of that ingredient.

Build and run to see the changes. Now create a new ingredient and select a color for it.

Ingredient form. Cursor picking the color from a list and saving the ingredient. List of ingredients with an ingredient with the picked color.

Great job! Now you can go ahead and list all the ingredients you need for that special potion you're brewing. :]

Where to Go From Here

You can download the final project by clicking the Download Materials button at the top or bottom of the tutorial.

In this SwiftUI Realm tutorial, you learned to create, update, fetch, and delete objects from Realm using SwiftUI. In addition to the basics, you also learned about migrations and how to create them.

To learn more about Realm, you can refer to its official documentation. And don't forget to check out our book, Realm: Building Modern Swift Apps with Realm Database.

If you want to learn more about SwiftUI, see our SwiftUI by Tutorials.

I hope you enjoyed this tutorial. If you have any questions or comments, please feel free to join the discussion below.

Average Rating

5/5

Add a rating for this content

3 ratings

More like this

Contributors

Comments