7

Realm with SwiftUI Tutorial: Getting Started [FREE]

 1 year ago
source link: https://www.raywenderlich.com/32960966-realm-with-swiftui-tutorial-getting-started
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Home iOS & Swift Tutorials

Realm with SwiftUI Tutorial: Getting Started

Learn how to use Realm with SwiftUI as a data persistence solution by building a potion shopping list app.

By Renan Benatti Dias Jun 14 2022 · Article (30 mins) · Intermediate

Version

RealmSwiftUI-feature.png
Update note: Renan Dias updated this tutorial for iOS 15, Swift 5.5 and Xcode 13. He also wrote the original version.

Realm Mobile Database is a popular object database management system. It’s open-source, and you can use it on multiple platforms. Realm aims to be a fast, performant, flexible and simple solution for persisting data while 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. Because 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 tracks all the ingredients you need to make magic potions! So grab your potions kit because it’s time to dive right into this cauldron. :]

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

Getting Started

Download the project materials by clicking 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, even for experienced wizards. This app helps you track 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!

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

List with 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 class is a representation of an ingredient.
  • IngredientListView.swift: This is the main view of the app. It displays a List. There’s one Section for ingredients to buy and another for bought ingredients.
  • IngredientRow.swift: This is the row view for each ingredient in the list.
  • IngredientFormView.swift: You’ll use this Form to create and update ingredients.

Right now, IngredientListView has two arrays of ingredients: one for those ingredients you need to buy and another for those you’ve already bought. However, when the user interacts with the app, nothing happens. You can’t add, delete or update any ingredient. Those are just mock ingredients.

You’ll fix this by adding Realm to the project and building a basic CRUD with it. But before you do that, it’s important to understand what Realm is.

Working With Realm

Realm was built to provide a database persistence and data query framework with a native, familiar-to-read language syntax. For iOS, iPadOS, watchOS and macOS, data objects are defined using Swift classes rather than from a separate database schema like Core Data. It’s also available for many other platforms, including but not limited to:

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

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 isn’t 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 set up Realm, you need to understand how it works.

Realm uses files to save and manage your database. Each Realm database in your app is called a realm. Your app might 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 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 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 want to learn more about it, check out Swift Package Manager for iOS tutorial.

To set up your dependency, select File ▸ Add Packages….

SPM window with a list of packages

Copy the following and paste into the combined search/input box:

https://github.com/realm/realm-swift

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

Select the Realm-Swift package and select Up to Next Major Version under dependency rule. Next, leave the version as 10.25.0. This makes sure your project will use the latest version of Realm. (As of writing this is therefore from 10.25.0.) Finally, click Add Package.

SPM window with Realm Swift package selected

Next, select both Package Products — Realm and RealmSwift — and click Add Package.

SPM window with Realm products selected

Xcode downloads the code from the repository and adds the Realm package to the PotionsMaster target.

Build and run to be sure everything is working.

List with ingredients and bought ingredients

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

Defining Your Realm Object Model

Open and look at Ingredient.swift, inside the Models group.

This class defines an ingredient in the domain of PotionsMaster. Notice it’s a plain Swift class with Published properties and an id to identify each object.

This object is used inside the list of ingredients and inside the form to create or edit an ingredient. However, if you try to modify any ingredient or create a new one, you see the list doesn’t change. Even though you can bind the properties of this class to the form, you’re not yet persisting any changes.

You’ll modify this object to store it in Realm now.

Still in Ingredient.swift, begin by adding the import for RealmSwift:

import RealmSwift

and then replace the declaration of the class with the following:

class Ingredient: Object, ObjectKeyIdentifiable {

Object is a type alias to RealmSwiftObject. This is the class Realm uses to store data inside a realm. By subclassing Ingredient to it, you’re able to store this class in the database.

Like Identifiable, ObjectKeyIdentifiable is a protocol Realm uses to identify Realm objects.

Next, replace the following lines:

@Published var title = ""
@Published var notes = ""
@Published var quantity = 1
@Published var bought = false

With the following code:

@Persisted var title = ""
@Persisted var notes = ""
@Persisted var quantity = 1
@Persisted var bought = false

@Persisted is a property wrapper like Core Data’s @NSManaged. It defines properties as managed by the Realm framework, allowing it to store their value.

Realm Property Types

Although you’re still using regular Swift classes to store objects in it, Realm has a limited set of platform-independent property types because of its cross-platform nature.

You can define properties with the following types:

  • Int, Int8, Int16, Int32, Int64
  • Double
  • Float
  • String
  • Decimal128

Every property of Ingredient is declared with a default required value. But you can declare optional properties, too. To define an optional property, all you have to do is make the property optional, like any other Swift property.

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 model.

Before you move on, find this code inside Ingredient.swift:

let id = UUID()

And change it to the following:

@Persisted(primaryKey: true) var id: ObjectId

This changes the ID from UUID to ObjectId and uses the Persisted(primaryKey:) to set this value as the primary key of this object. The framework uses this property to enforce uniqueness of objects and to fetch objects from Realm.

Now that you’ve defined your Realm object, it’s time to write code to fetch and add objects to your database.

Fetching Realm Objects

Fetching objects from Realm is simple. You can instantiate a realm and fetch objects from it. However, the Realm team created handy property wrappers, which are like Core Data’s FetchRequest, to use Realm with SwiftUI.

You’ll use them to fetch objects from Realm and observe changes to the database to update the view whenever you add, edit or delete an object from Realm.

Inside Views, open IngredientListView.swift. Add the RealmSwift import:

import RealmSwift

and find the following two State properties:

@State var ingredients: [Ingredient] = []
@State var boughtIngredients: [Ingredient] = []

Replace them with the following:

// 1
@ObservedResults(
  // 2
  Ingredient.self,
  // 3
  where: { $0.bought == false }
) var ingredients

// 4
@ObservedResults(
  Ingredient.self,
  where: { $0.bought == true }
) var boughtIngredients

Here’s a breakdown of the code:

  1. You define a property, ingredients, and mark it with @ObservedResults.
  2. The first parameter of @ObservedResults is the type of the object you want to fetch from Realm: in this case, Ingredient.
  3. You also use a where closure argument to filter for only the ingredients that haven’t been bought.
  4. Here, you define another property, boughtIngredients, and also mark it with @ObservedResults. However, this time, you filter the ingredients that have been bought.
Note: Support for the where closure argument was recently added. It’s still OK to use the previous filter argument calling style that takes an NSPredicate to filter your objects like Core Data does. For example:
@ObservedResults(
  Ingredient.self,
  filter: NSPredicate(format: "bought == true")
) var boughtIngredients

However, it’s often better to use the where form of object filtering, because that tells the Swift compiler to check that the bought property of is valid syntax. If you write a predicate string incorrectly or update the property names of the data class without also updating the predicate string, it might result in a runtime error because Swift can’t check inside the string for valid syntax!

Understanding @ObservedResults

@ObservedResults is a property wrapper you can use to fetch and observe objects from a realm. This property fetches objects and returns a Results type, which is a type from Realm that represents a collection of objects retrieved from queries.

By replacing both arrays in your view and adding those two properties, you’re telling Realm to fetch ingredients from the database and display them in the view. And you can even filter the results, just like you’re doing with the bought ingredients.

This is a powerful property wrapper. Whenever you add or remove objects that are ingredients in the database, Realm updates those properties and SwiftUI updates its views.

Before you move on, open ContentView.swift and replace the code inside NavigationView with the following:

IngredientListView()

This instantiates an IngredientListView without passing any values to the properties.

Build and run the project.

Empty list of ingredients

You’re not seeing the mocked data anymore and the list is gone. That’s because the database is empty and there’s nothing to list. It’s time to populate the database with objects.

Adding Objects to the Database

Open IngredientFormView.swift and import RealmSwift at the top of the file.

import RealmSwift

Next, find the following property:

@ObservedObject var ingredient: Ingredient

And replace it with the following:

@ObservedRealmObject var ingredient: Ingredient

ObservedRealmObject is a Realm property wrapper that behaves much like ObservedObject. You use it to observe changes to the state of Realm objects and update the view whenever the object changes. You’ll also use it to update the object in the database later.

Next, at the top of the view, add the following environment property:

@Environment(\.realm) var realm

This environment property stores the default realm you use to store and fetch objects from the database.

Finally, find the method save() at the bottom of the file and replace the contents of it with the following:

try? realm.write {
  realm.add(ingredient)
}
dismiss()

Here, you start a write transaction by calling write(withoutNotifying:_:) on the Realm object. 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 Ingredient to Realm. Realm now stores the object and tracks its changes, making it a managed object.

Build and run. Tap New Ingredient and type a title and note. Save the ingredient to see it in your list.

Ingredients list showing ginger

Success! You can now add and list objects in the app. When you open IngredientFormView, you also pass an empty ingredient object to it. When you add this ingredient in the database, Realm updates the property ingredients inside IngredientListView and SwiftUI updates your 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 IngredientRow.swift and import RealmSwift.

import RealmSwift

Next, replace the following line:

let ingredient: Ingredient  

With the following:

@ObservedRealmObject var ingredient: Ingredient

Once again, you’re using ObservedRealmObject to observe changes to an ingredient in the database and update the view when its properties change.

Next, find and replace the contents of toggleBought() with the following:

$ingredient.bought.wrappedValue.toggle()

This code toggles the bought property of that ingredient to true if the ingredient hasn’t been bought yet, or to false if the ingredient has already been bought.

Notice the $ before the ingredient. Instead of calling write(withoutNotifying:_ :) to start a write transaction, you use $ in your Realm object to automatically start a write transaction and update the value of bought.

Build and run. Tap the circular icon on the right of the row to buy an ingredient.

Ingredient in the bought section

Awesome! You update ingredient, and Realm notifies the results inside IngredientListView to update the list. How cool is that? :]

You can also update objects by starting a write transaction and updating each property of the object inside that transaction.

You’ll do this next to update the rest of the properties of an ingredient.

Updating Other Properties

Now that you know how to update an object, you can use Realm to update other properties just as easily. Go back to IngredientFormView.swift and replace the computed property isUpdating with the following code:

var isUpdating: Bool {
  ingredient.realm != nil
}

This updates isUpdating to check if the ingredient in the form has been persisted in the database. When you tap New Ingredient, you pass to IngredientFormView a new instance of Ingredient. This is an unmanaged object. That means the database doesn’t know about it yet and any changes won’t persist until you add it to Realm.

When you open IngredientFormView with a new instance of Ingredient, this instance doesn’t have a realm yet, meaning it has yet to be saved in Realm. However, when you open IngredientFormView by tapping a row and passing its ingredient to the form, this ingredient has already been stored in the database and, thus, you’re updating it.

Build and run. Tap an ingredient in the list to open the form.

Ingredient form updating

Update its properties and tap Done.

List with ingredient in the bought section

Fantastic! You didn’t have to change a single line of code in the body of the view to update the ingredient. Realm objects have bindings to SwiftUI controls and, much like toggling the bought property, they already start a realm transaction to write the changes in the database whenever you change those values.

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. You can tap the blue icon again to move it back, but you can’t remove it from the list.

You’ll use the List‘s swipe gesture to remove ingredients from the list.

Open IngredientListView.swift and find the following code in the second Section:

ForEach(boughtIngredients) { ingredient in
  IngredientRow(ingredient: ingredient)
}

Next, add the following just under the ForEach view:

.onDelete(perform: $boughtIngredients.remove)

onDelete(perform:) is a view modifier for adding a delete action to the row of a list. It passes an IndexSet of the items to remove from the list.

You use Realm’s Results type to remove(atOffsets:) binding for removing those objects from Realm. This creates a write transaction and removes the swiped ingredient from Realm. Realm takes care of updating boughtIngredients, and SwiftUI updates the list.

Build and run the app. Buy ingredients and swipe left to delete them from the database.

List with bought ingredient being swiped left to delete it

Adding a New Property to a Realm Object

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

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

Inside the Models group, create a new swift file and name it ColorOptions.swift. Add the following code to the new file:

// 1
import SwiftUI
import RealmSwift

// 2
enum ColorOptions: String, CaseIterable, PersistableEnum {

  // 3
  case green
  case lightBlue
  case lightRed

  // 4
  var color: Color {
    Color(rawValue)
  }

  // 5
  var title: String {
    switch self {
    case .green:
      return "Green"
    case .lightBlue:
      return "Light Blue"
    case .lightRed:
      return "Light Red"
    }
  }
}

Here’s a breakdown of this enum:

  1. First, you import SwiftUI and RealmSwift
  2. Then, you define an enum named ColorOptions and extend it to String. You also conform it to CaseIterable, to list all cases of the enum and to PersistableEnum. This is a protocol Realm uses to store enum values in the database.
  3. Next, you define three cases, green, lightBlue and lightRed. Those are the color options users will choose for each ingredient.
  4. Here, you add a computed property to instantiate a Color from the project assets.
  5. Finally, you define another computed property for the title of each case.

Now, open Ingredient.swift and add a new property under bought:

@Persisted var colorOption = ColorOptions.green

You add this property to store a color option with the ingredient in the database.

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

Storing the New Property in Realm

Now that you’re ready to store the new color, it’s time to update IngredientFormView.swift to let users select the ingredient color. You’ll use a Picker view to allow users to select the color from the available options.

Open IngredientFormView.swift and add the following property:

let colorOptions = ColorOptions.allCases

This adds a property to store all cases of ColorOption. You’ll use this to list all color options in the picker view.

Next, find the comment // To Do Add Color Picker and replace it with the code below:

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

This adds a Picker view to the form, binding the property colorOption form to the ingredient, with all the cases from ColorOption.

Build and run. And you get …

Xcode console Realm migration crash

The app crashes as soon as you open it! 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 that subclass Object. When it finds one, it creates a schema for mapping the model to the database.

When you change an object and add a property, there’s a mismatch between the new schema and the one in the database. If that happens, Realm throws an error. It doesn’t know what it should do with the old objects without the new property. 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.

Because you added a new property, colorOption, to Ingredient, 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. Another way to solve this, during development, is simply deleting the app and building and running it again. This deletes the realm on disk and creates a brand new one from scratch.

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: Ingredient.className()
      ) { _, newObject in
        newObject?["colorOption"] = ColorOptions.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 Ingredient objects in Realm, you assign a default value to the new property, colorOption.

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

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

static var configuration: Realm.Configuration {
  Realm.Configuration(schemaVersion: 1, migrationBlock: migrationBlock)
}

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

You'll use this configuration with your default realm now.

Open AppMain.swift and add the following line just below ContentView:

.environment(\.realmConfiguration, RealmMigrator.configuration)

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.

Build and run again. This time the crash is gone!

Screenshot of PotionsMaster app working once again

You've added a new property to Ingredient. And you've taken care of the migration. Now it's time to update the row so users can see the color they choose!

Updating Ingredient Row

Open Ingredient.swift and add the following extension at the bottom of the file:

extension Ingredient {
  var color: Color {
    colorOption.color
  }
}

This adds a convenient computed property to get the color of the selected option.

Next, open IngredientRow.swift and find the following code:

Button(action: toggleBought) {
  Image(systemName: buttonImage)
    .resizable()
    .frame(width: 24, height: 24)
}

Add the following view modifier to the image, inside the button:

.foregroundColor(ingredient.color)

Here, you change the foreground color of the icon to toggle an ingredient as bought to the color the user selects for that ingredient.

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

Ingredients list with colored items

Great job! Now, you can 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.

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

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

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK