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 iOS 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:

  • Implicitly open a realm with a defaultConfiguration, or specify a different configuration. This works for both non-synced and synced realms.
  • Always download changes before opening a synced realm, which times out when the user is offline.
  • Open a synced realm even when a user is offline, but may lack the most recent data.

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(Group.self)
@ObservedResults(Group.self) var groups

When you do not specify a configuration, these property wrappers use the defaultConfiguration. However, you can use environment injection to specify a different configuration.

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

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

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.

/// This view opens a synced realm.
struct OpenSyncedRealmView: View {
// Use AsyncOpen to download the latest changes from
// your Realm app before opening the realm.
// Leave the `partitionValue` an empty string to get this
// value from the environment object passed in above.
@AsyncOpen(appId: YOUR_REALM_APP_ID_HERE, partitionValue: "", timeout: 4000) var asyncOpen
var body: some View {
switch asyncOpen {
// Starting the Realm.asyncOpen 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!
}(), 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)
}
}
}

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

@AutoOpen(appId: "app_id", partitionValue: <partition_value>) var autoOpen

This SwiftUI property wrapper attempts to initiate a Realm.asyncOpen() for the current user. If there is no internet connection, this property wrapper instead returns an opened realm for the given appId and partitionValue.

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 @AutoOpen 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.

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_REALM_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)
}
}
}

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(Group.self)
@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)
} 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())
}
}
}
}

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.

When you want to pass a synced realm, 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_REALM_APP_ID_HERE, partitionValue: "", timeout: 4000) var asyncOpen

You can inject an opened realm into a view as an environment value. The property wrapper uses this 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 group. Implements functionality for adding, rearranging,
/// and deleting items in the group.
struct ItemsView: View {
/// The group is a container for a list of items. Using a group instead of all items
/// directly allows us to maintain a list order that can be updated in the UI.
@ObservedRealmObject var group: Group
/// 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(group.items) { item in
ItemRow(item: item)
}.onDelete(perform: $group.items.remove)
.onMove(perform: $group.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.
$group.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)
}
}

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(Group.self)
@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)
} 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())
}
}
}
}
Give Feedback
MongoDB logo
© 2021 MongoDB, Inc.

About

  • Careers
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2021 MongoDB, Inc.