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

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(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())
}
}
}
}
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_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)
}
}
}

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
←  Flexible Sync - Swift SDKLegacy Realm 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.