Docs Menu

Use Realm Database with SwiftUI

On this page

The Realm Swift SDK offers features to simplify development with SwiftUI. This page provides an overview of those features.

Tip
See also:
  • Xcode project using the SwiftUI "App" template. To use all of the Realm Swift SDK's SwiftUI features, the minimum iOS target is 15.0. Some features are compatible with older versions of iOS.
  • Install the Swift SDK. Use the most recent version of the Realm Swift SDK to get all of the features and enhancements for SwiftUI.

The Swift SDK provides several different property wrappers that make it easier to open a realm.

You can:

When you use @ObservedRealmObject or @ObservedResults, these property wrappers implicitly open a realm and retrieve the specified objects or results.

// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(ItemGroup.self) var itemGroups

When you do not specify a configuration, these property wrappers use the defaultConfiguration. You can set the defaultConfiguration globally, and property wrappers across the app can use that configuration when they implicitly open a realm.

You can provide alternative configurations that the property wrappers use to implicitly open the realm. You might want to do this when using multiple configurations in your app, as in cases where you have both a SyncConfiguration and a local Configuration. To do this, create explicit configurations. Then, use environment injection to pass the respective configurations to the views that need them. Passing a configuration to a view where property wrappers open a realm uses the passed configuration instead of the defaultConfiguration.

New in version 10.12.0.

These SwiftUI property wrappers open synced realms and populate views. The main difference between these property wrappers is whether the user must be online:

  • To download updates from your Atlas App Services app before opening a realm, use the @AsyncOpen property wrapper. This requires the user to have a network connection.
  • To open a synced realm regardless of whether the user has a network connection, use the @AutoOpen property wrapper. This property wrapper enables developers to design offline-first capabilities into their apps.

Use the @AsyncOpen property wrapper for apps that require up-to-date information from the server, such as game apps with live leaderboards that the user can play on multiple devices. This ensures the user is never using the app with stale data.

This SwiftUI property wrapper initiates Realm.asyncOpen() for the current user. The property wrapper publishes states, represented by the AsyncOpenState enum, which you can use to update the view.

Example

This example illustrates one way you might use @AsyncOpen to open a realm in a view. First, check for a user, or log them in. Then, attempt to open the realm, switching on the AsyncOpenState to display an appropriate view. When the realm opens successfully, inject it as an environment value to populate the view.

Like @AsyncOpen, @AutoOpen attempts to download updates before opening the realm. However, if a network connection is not available, this method instead opens a realm with data on the device.

Use this property wrapper for apps where it's not a problem for the user to work with potentially stale data, such as note-taking apps where users should be able to work with data on the device

This SwiftUI property wrapper attempts to download updates before opening a realm for the current user. If there is no internet connection, this property wrapper instead returns the most up-to-date version of the local realm file for the given appId and Flexible Sync or Partition-Based Sync configuration.

The property wrapper publishes states, represented by the AsyncOpenState enum, which you can use to update the view. For a full example, see the @AsyncOpen code examples above.

The Realm Swift SDK provides several ways to pass realm data between views:

  • Pass Realm objects to a view
  • Use environment injection to:

    • Inject a partition value into a view
    • Inject an opened realm into a view
    • Inject a realm configuration into a view

When you use the @ObservedRealmObject or @ObservedResults property wrapper, you implicitly open a realm and retrieve the specified objects or results. You can then pass those objects to a view further down the hierarchy.

/// The main content view if not using Sync.
struct LocalOnlyContentView: View {
// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(ItemGroup.self) var itemGroups
var body: some View {
if let itemGroup = itemGroups.first {
// Pass the ItemGroup objects to a view further
// down the hierarchy
ItemsView(itemGroup: itemGroup)
} else {
// For this small app, we only want one itemGroup in the realm.
// You can expand this app to support multiple itemGroups.
// For now, if there is no itemGroup, add one here.
ProgressView().onAppear {
$itemGroups.append(ItemGroup())
}
}
}
}

Environment injection is a useful tool in SwiftUI development with Realm Database. Realm property wrappers provide different ways for you to work with environment values when developing your SwiftUI application.

If you are using Partition-Based Sync, you can use environment injection to pass the .partitionValue environment value. Inject this into a view where you perform the @AsyncOpen or @AutoOpen:

OpenSyncedRealmView().environment(\.partitionValue, user.id)

Then, when you use the property wrapper to open a synced realm, leave the partitionValue an empty string. The property wrapper populates the value from the environment object passed in from above.

@AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, partitionValue: "", timeout: 4000) var asyncOpen

You can inject a realm that you opened in another SwiftUI view into a view as an environment value. The property wrapper uses this passed-in realm to populate the view:

ListView()
.environment(\.realm, realm)

You can use a realm other than the default realm by passing a different configuration in an environment object.

LocalOnlyContentView()
.environment(\.realmConfiguration, Realm.Configuration( /* ... */ ))

The Swift SDK provides the @ObservedRealmObject property wrapper that invalidates a view when an observed object changes. You can use this property wrapper to create a view that automatically updates itself when the observed object changes, such as a new item being added to a group.

/// The screen containing a list of items in a itemGroup. Implements functionality for adding, rearranging,
/// and deleting items in the itemGroup.
struct ItemsView: View {
/// The itemGroup is a container for a list of items. Using an itemGroup instead of all items
/// directly allows us to maintain a list order that can be updated in the UI.
@ObservedRealmObject var itemGroup: ItemGroup
/// The button to be displayed on the top left.
var leadingBarButton: AnyView?
var body: some View {
NavigationView {
VStack {
// The list shows the items in the realm.
List {
ForEach(itemGroup.items) { item in
ItemRow(item: item)
}.onDelete(perform: $itemGroup.items.remove)
.onMove(perform: $itemGroup.items.move)
}.listStyle(GroupedListStyle())
.navigationBarTitle("Items", displayMode: .large)
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: self.leadingBarButton,
// Edit button on the right to enable rearranging items
trailing: EditButton())
// Action bar at bottom contains Add button.
HStack {
Spacer()
Button(action: {
// The bound collection automatically
// handles write transactions, so we can
// append directly to it.
$itemGroup.items.append(Item())
}) { Image(systemName: "plus") }
}.padding()
}
}
}
}

New in version 10.19.0.

The Realm Swift SDK allows you to extend .searchable. When you use ObservedResults to query a realm, you can specify collection and keypath in the result set to denote it is searchable.

// The list shows the items in the realm.
List {
ForEach(items) { item in
ItemRow(item: item)
}
}
.searchable(text: $searchFilter,
collection: $items,
keyPath: \.name) {
ForEach(items) { itemsFiltered in
Text(itemsFiltered.name).searchCompletion(itemsFiltered.name)
}
}

The @ObservedResults property wrapper opens a realm and returns all objects of the specified type. However, you can filter or query @ObservedResults to use only a subset of the objects in your view.

To filter @ObservedResults using the NSPredicate Query API, pass an NSPredicate as an argument to filter:

@ObservedResults(Item.self, filter: NSPredicate(format: "isFavorite == true")) var items

New in version 10.24.0: Use where to perform type-safe queries on ObservedResults.

To use @ObservedResults with the Realm Type-Safe Query API, pass a query in a closure as an argument to where:

@ObservedResults(Item.self, where: ( { $0.isFavorite == true } )) var items

In addition to performing writes inside a transaction block, the Realm Swift SDK offers a convenience feature to enable quick writes outside of a transaction.

When you use the @ObservedRealmObject or @ObservedResults property wrappers, you can implicitly open a write transaction. Use the $ operator to create a two-way binding to one of the state object's properties. Then, when you update this value, you initiate an implicit write.

In this example, we create two-way bindings with two of the state object's properties:

  • $item.name creates a binding to the model Item object's name property
  • $item.isFavorite creates a binding to the model Item object's isFavorite property

When the app user updates those fields in this example, Realm Database opens an implicit write transaction and saves the new values to the database.

struct ItemDetailsView: View {
@ObservedRealmObject var item: Item
var body: some View {
VStack(alignment: .leading) {
Text("Enter a new name:")
// Accept a new name
TextField("New name", text: $item.name)
.navigationBarTitle(item.name)
.navigationBarItems(trailing: Toggle(isOn: $item.isFavorite) {
Image(systemName: item.isFavorite ? "heart.fill" : "heart")
})
}.padding()
}
}
/// The main content view if not using Sync.
struct LocalOnlyContentView: View {
// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(ItemGroup.self) var itemGroups
var body: some View {
if let itemGroup = itemGroups.first {
// Pass the ItemGroup objects to a view further
// down the hierarchy
ItemsView(itemGroup: itemGroup)
} else {
// For this small app, we only want one itemGroup in the realm.
// You can expand this app to support multiple itemGroups.
// For now, if there is no itemGroup, add one here.
ProgressView().onAppear {
$itemGroups.append(ItemGroup())
}
}
}
}
Tip
See also: Swift SDK Migration Logic

For more general information about migration logic in the Realm Swift SDK, such as how to increment a schema or write a migration block, see: Modify an Object Schema - Swift SDK.

To perform a migration:

  • Update your schema and write a migration block, if required
  • Specify a Realm.Configuration that uses this migration logic and/or updated schema version when you initialize your realm.

From here, you have two options to pass the configuration object. You can either:

  • Use environment injection to provide this configuration to the first view in your hierarchy that uses Realm
  • Explicitly provide the configuration to a Realm property wrapper that takes a configuration object, such as @ObservedResults or @AsyncOpen.
Example

For example, you might want to add a property to an existing object, such as the Item object in the SwiftUI Quick Start:

/// Users can enter a description, which is an empty string by default
@Persisted var itemDescription = ""

After you add your new property to the schema, you must increment the schema version. Your Realm.Configuration might look like this:

let config = Realm.Configuration(schemaVersion: 2)

Declare this configuration somewhere that is accessible to the first view in the hierarchy that needs it. Declaring this above your @main app entrypoint makes it available everywhere, but you could also put it in the file where you first open a realm.

Once you have declared the configuration, you can pass it as an environment object to the first view in your hierarchy that opens a realm. If you are using the @ObservedResults or @ObservedRealmObject property wrappers, these views implicitly open a realm, so they also need access to this configuration.

.environment(\.realmConfiguration, config)

In the SwiftUI quickstart, the first view in the hiearchy that opens a realm varies depending on whether you're using the app with or without Sync.

Without sync, you can pass the realm configuration environment object directly to the LocalOnlyContentView:

@main
struct ContentView: SwiftUI.App {
var body: some Scene {
WindowGroup {
// Using Sync?
if let app = app {
SyncContentView(app: app)
} else {
LocalOnlyContentView()
.environment(\.realmConfiguration, config)
}
}
}
}

Which opens a realm implicitly with:

struct LocalOnlyContentView: View {
@State var searchFilter: String = ""
@ObservedResults(Group.self) var groups
var body: some View {
if let group = groups.first {
// Pass the Group objects to a view further
// down the hierarchy
ItemsView(group: group, searchFilter: $searchFilter)
} else {
// For this small app, we only want one group in the realm.
// You can expand this app to support multiple groups.
// For now, if there is no group, add one here.
ProgressView().onAppear {
$groups.append(Group())
}
}
}
}

However, for the Sync version of the quickstart, you open the Realm explicitly using the @AsyncOpen or @AutoOpen property wrapper:

struct OpenSyncedRealmView: View {
// @AutoOpen attempts to connect to the server and download remote changes
// before the realm opens, which might take a moment. However, if there is
// no network connection, AutoOpen will open a realm on the device.
// We can use an empty string as the partitionValue here because we're
// injecting the user.id as an environment value from the LoginView.
@AutoOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, partitionValue: "", timeout: 4000) var autoOpen
var body: some View {
switch autoOpen {
// Starting the Realm.autoOpen process.
// Show a progress view.
case .connecting:
ProgressView()
// Waiting for a user to be logged in before executing
// Realm.asyncOpen.
case .waitingForUser:
ProgressView("Waiting for user to log in...")
// The realm has been opened and is ready for use.
// Show the content view.
case .open(let realm):
ItemsView(group: {
if realm.objects(Group.self).count == 0 {
try! realm.write {
realm.add(Group())
}
}
return realm.objects(Group.self).first!
}(), searchFilter: $searchFilter, leadingBarButton: AnyView(LogoutButton())).environment(\.realm, realm)
// The realm is currently being downloaded from the server.
// Show a progress view.
case .progress(let progress):
ProgressView(progress)
// Opening the Realm failed.
// Show an error view.
case .error(let error):
ErrorView(error: error)
}
}
}

So you must pass pass the environment object to the OpenSyncedRealmView:

struct SyncContentView: View {
// Observe the Realm app object in order to react to login state changes.
@ObservedObject var app: RealmSwift.App
var body: some View {
if let user = app.currentUser {
// If there is a logged in user, pass the user ID as the
// partitionValue to the view that opens a realm.
OpenSyncedRealmView()
.environment(\.partitionValue, user.id)
.environment(\.realmConfiguration, config)
} else {
// If there is no user logged in, show the login view.
LoginView()
}
}
}

The important thing to remember is to make sure to pass the Realm.Configuration that encompasses your migration logic to any view hierarchy that implicitly or explicitly opens a realm.

You can explicitly pass the configuration object to a Realm SwiftUI property wrapper that takes a configuration object, such as @ObservedResults or @AutoOpen. In this case, you might pass it directly to @ObservedResults in our ItemsView.

@ObservedResults(Item.self, configuration: config) var items

In the simplest case, you can use SwiftUI Previews with one or more objects that use Realm properties you can set directly at initialization. You might want to do this when previewing a Detail view. Consider our ItemDetailsView:

/// Represents a screen where you can edit the item's name.
struct ItemDetailsView: View {
@ObservedRealmObject var item: Item
var body: some View {
VStack(alignment: .leading) {
Text("Enter a new name:")
// Accept a new name
TextField("New name", text: $item.name)
.navigationBarTitle(item.name)
.navigationBarItems(trailing: Toggle(isOn: $item.isFavorite) {
Image(systemName: item.isFavorite ? "heart.fill" : "heart")
})
}.padding()
}
}

Create an extension for your model object. Where you put this extension depends on convention in your codebase. You may put it directly in the model file, have a dedicated directory for sample data, or use some other convention in your codebase.

In this extension, initialize one or more Realm objects with static let:

extension Item {
static let item1 = Item(value: ["name": "fluffy coasters", "isFavorite": false, "ownerId": "previewRealm"])
static let item2 = Item(value: ["name": "sudden cinder block", "isFavorite": true, "ownerId": "previewRealm"])
static let item3 = Item(value: ["name": "classy mouse pad", "isFavorite": false, "ownerId": "previewRealm"])
}

In this example, we initialize objects with a value. You can only initialize objects with a value when your model contains properties that you can directly initialize. If your model object contains properties that are only mutable within a write transaction, such as a List property, you must instead create a realm to use with your SwiftUI Previews.

After you have initialized an object as an extension of your model class, you can use it in your SwiftUI Preview. You can pass the object directly to the View in the Preview:

struct ItemDetailsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ItemDetailsView(item: Item.item2)
}
}
}

When you use @ObservedResults in a List view, this implicitly opens a realm and queries it. For this to work in a Preview, you need a realm populated with data. As an alternative, you can conditionally use a static array in Previews and only use the @ObservedResults variable when running the app.

You could do this in multiple ways, but for the sake of making our code easier to read and understand, we'll create an EnvironmentValue that can detect whether the app is running in a Preview:

import Foundation
import SwiftUI
public extension EnvironmentValues {
var isPreview: Bool {
#if DEBUG
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
#else
return false
#endif
}
}

Then, we can use this as an environment value in our view, and conditionally change which variable we use based on whether or not we are in a Preview.

This example builds on the Item extension we defined above. We'll create an itemArray as a static let in our Item extension, and include the item objects we already created:

static let itemArray = [item2, item2, item3]

Then, when we iterate through our List, use the static itemArray if running in a Preview, or use the @ObservedResults query if not in a Preview.

struct ItemListView: View {
@Environment(\.isPreview) var isPreview
@ObservedResults(Item.self) var items
var previewItems = Item.itemArray
var body: some View {
NavigationView {
VStack {
List {
if isPreview {
ForEach(previewItems) { item in
ItemRow(item: item)
}
} else {
ForEach(items) { item in
ItemRow(item: item)
}.onDelete(perform: $items.remove)
.onMove(perform: $items.move)
}
}
... More View code

This has the benefit of being lightweight and not persisting any data, but the downside of making the View code more verbose. If you prefer cleaner View code, you can create a realm with data that you use in the Previews.

In some cases, your only option to see realm data in a SwiftUI Preview is to create a realm that contains the data. You might do this when populating a property that can only be populated during a write transaction, rather than initialized directly with a value, such as a List or MutableSet. You might also want to do this if your view relies on more complex object hierarchies being passed in from other views.

However, using a realm directly does inject state into your SwiftUI Previews, which can come with drawbacks. Whether you're using Realm or Core Data, stateful SwiftUI Previews can cause issues like:

  • Seeing unexpected or duplicated data due to re-running the realm file creation steps repeatedly
  • Needing to perform a migration within the SwiftUI Preview when you make model changes
  • Potential issues related to changing state within views
  • Unexplained crashes or performance issues related to issues that are not surfaced in a visible way in SwiftUI Previews

You can avoid or fix some of these issues with these tips:

You can create a static variable for your realm in your model extension. This is where you do the work to populate your realm. In our case, we create an ItemGroup and append some Item objects to the items List property. This example builds on the example above where we initialized a few Item objects in an Item extension.

We'll create an ItemGroup extension, and create a single ItemGroup object in that extension. Then, we'll create a previewRealm by adding the ItemGroup we just created, and appending the example Item objects from the Item extension.

To avoid adding these objects more than once, we add a check to see if the ItemGroup already exists by querying for ItemGroup objects and checking that the count is 1. If the realm contains an ItemGroup, we can use it in our SwiftUI Preview. If not, we add the data.

extension ItemGroup {
static let itemGroup = ItemGroup(value: ["ownerId": "previewRealm"])
static var previewRealm: Realm {
var realm: Realm
let identifier = "previewRealm"
let config = Realm.Configuration(inMemoryIdentifier: identifier)
do {
realm = try Realm(configuration: config)
// Check to see whether the in-memory realm already contains an ItemGroup. If it does, we'll just return the existing realm. If it doesn't, we'll add an ItemGroup and append the Items.
let realmObjects = realm.objects(ItemGroup.self)
if realmObjects.count == 1 {
return realm
} else {
try realm.write {
realm.add(itemGroup)
itemGroup.items.append(objectsIn: [Item.item1, Item.item2, Item.item3])
}
return realm
}
} catch let error {
fatalError("Can't bootstrap item data: \(error.localizedDescription)")
}
}
}

To use it in the SwiftUI Preview, our ItemsView code expects an ItemGroup. In our Preview, we can get the realm, query it for the ItemGroup, and pass it to the view:

struct ItemsView_Previews: PreviewProvider {
static var previews: some View {
let realm = ItemGroup.previewRealm
let itemGroup = realm.objects(ItemGroup.self)
ItemsView(itemGroup: itemGroup.first!)
}
}

If you don't have a View that is expecting a realm object to be passed in, but instead uses @ObservedResults to query a realm or otherwise work with an existing realm, you can inject the realm into the view as an environment value:

struct SomeListView_Previews: PreviewProvider {
static var previews: some View {
SomeListView()
.environment(\.realm, ItemGroup.previewRealm)
}
}

When possible, use an in-memory realm to get around some of the state-related issues that can come from using a database within a SwiftUI Preview.

Use the inMemoryIdentifier configuration property when you initialize the realm.

static var previewRealm: Realm {
var realm: Realm
let identifier = "previewRealm"
let config = Realm.Configuration(inMemoryIdentifier: identifier)
do {
realm = try Realm(configuration: config)
... Add data to realm
Note

Do not use the the deleteRealmIfMigrationNeeded configuration property when you initialize a realm for SwiftUI Previews. Due to the way Apple has implemented SwiftUI Previews, using this property to bypass migration issues causes SwiftUI Previews to crash.

If you run into other SwiftUI Preview issues related to state, such as a failure to load a realm in a Preview due to migration being required, there are a few things you can do to remove cached Preview data.

The Apple-recommended fix is to close Xcode and use the command line to delete all your existing SwiftUI Preview data.

  1. Close Xcode.
  2. From your command line, run:

    xcrun simctl --set previews delete all

It's possible that data may persist after running this command. This is likely due to Xcode retaining a reference due to something in the Preview and being unable to delete it. You can also try these steps to resolve issues:

  • Build for a different simulator
  • Restart the computer and re-run xcrun simctl --set previews delete all
  • Delete stored Preview data directly. This data is stored in ~/Library/Developer/Xcode/UserData/Previews.

If you have an unexplained SwiftUI Preview crash when using realm, first try running the application on the simulator. The error messaging and logs available for the simulator make it easier to find and diagnose issues. If you can debug the issue in the simulator, this is the easiest route.

If you cannot replicate a SwiftUI Preview crash in the simulator, you can view crash logs for the SwiftUI Preview app. These logs are available in ~/Library/Logs/DiagnosticReports/. These logs sometimes appear after a delay, so wait a few minutes after a crash if you don't see the relevant log immediately.

If your app uses Atlas Device Sync, you may wonder how to use a synced realm in your SwiftUI Previews. A better practice is to use static objects or a local realm that you populate with data for your SwiftUI Previews.

In our example app, we can preview a view associated with Device Sync - the LoginView - without needing to use a realm at all:

struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}

Since we're only viewing the static UI, we don't need to worry about the SyncContentView that contains the logic of whether to show the LoginView or go to the OpenSyncedRealmView. We can also skip previewing the OpenSyncedRealmView, because that just handles logic associated with opening a synced realm and populating it for the ItemsView. So the next view we want to see in a Preview is the ItemsView.

Fortunately, with Realm, the code to work with the realm doesn't care whether the realm uses Device Sync or not - you work with the realm in the same way. So we can use the same local realm that we created in the example above in the SwiftUI Preview. The only difference is that we would want to show the LogoutButton on the SwiftUI Preview to see all of the UI components in the Preview:

struct ItemsView_Previews: PreviewProvider {
static var previews: some View {
let realm = ItemGroup.previewRealm
let itemGroup = realm.objects(ItemGroup.self)
ItemsView(itemGroup: itemGroup.first!, leadingBarButton: AnyView(LogoutButton()))
}
}

This is the same as our code above for the non-Synced realm, but we pass the LogoutButton() here to see it in the SwiftUI Preview. However, the LogoutButton View code disables the logout button unless there is a logged-in app.currentUser. Because there is no logged-in user in our SwiftUI Preview, the Logout button is grayed out to indicate that it is disabled. When you run the app in the simulator, you see it working as expected because you have a logged-in user there.

←  Flexible Sync - Swift SDKLegacy Sync Open Methods - Swift SDK →
Give Feedback
© 2022 MongoDB, Inc.

About

  • Careers
  • Investor Relations
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2022 MongoDB, Inc.