Docs Menu

Docs HomeRealm

Manage Flexible Sync Subscriptions - Swift SDK

On this page

  • Overview
  • About the Examples on This Page
  • Subscribe to Queryable Fields
  • Add a Subscription
  • Wait for Subscription Changes to Sync
  • Subscription Set State
  • Update Subscriptions with a New Query
  • Remove Subscriptions
  • Group Updates for Improved Performance
  • Flexible Sync RQL Limitations
  • Unsupported Query Operators in Flexible Sync
  • List Queries
  • Embedded or Linked Objects

New in version 10.22.0.

Flexible Sync uses subscriptions and permissions to determine which data to sync with your App.

To use Flexible Sync in an iOS client:

You can add, update, and remove query subscriptions to determine which data syncs to the client device.

If your app uses Data Ingest to unidirectionally sync AsymmetricObjects via an Atlas App Services App, you cannot create subscriptions for those objects.

Tip

See also:

This page details how to manage subscriptions for Flexible Sync.

For general information about using Device Sync with the Swift SDK, such as how to sync changes in the background or pause a sync session, see: Sync Changes Between Devices.

For information about setting up permissions for Flexible Sync, see: Flexible Sync Rules & Permissions.

Important

Flexible Sync does not support all the operators available in Realm Query Language. See Flexible Sync RQL Limitations for details.

The examples on this page use a simple data set for a task list app. The two Realm object types are Team and Task. A Task has a taskName, assignee's name, and completed flag. There is also a count of minutes spent working on it, and a due date. A Team has a teamName, zero or more Tasks, and a list of members.

class Task: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var taskName: String
@Persisted var assignee: String?
@Persisted var completed: Bool
@Persisted var progressMinutes: Int
@Persisted var dueDate: Date
}
class Team: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var teamName: String
@Persisted var tasks: List<Task>
@Persisted var members: List<String>
}

When you configure Flexible Sync on the backend, you specify which fields your client application can query. In the client application, use the subscriptions API to manage a set of subscriptions to specific queries on queryable fields.

You can:

  • Add subscriptions

  • React to subscription state

  • Update subscriptions with new queries

  • Remove individual subscriptions or all subscriptions for an object type

Data matching the subscription, where the user has the appropriate permissions, syncs between devices and the backend application.

You can specify an optional string name for your subscription.

When you create a subscription, Realm looks for data matching a query on a specific object type. You can have multiple subscription sets on different object types. You can also have multiple queries on the same object type.

Important

Object Links

You must add both an object and its linked object to the subscription set to see a linked object.

If your subscription results contain an object with a property that links to an object not contained in the results, the link appears to be nil. There is no way to distinguish whether that property's value is legitimately nil, or whether the object it links to exists but is out of view of the query subscription.

Example

You can create a subscription with an explicit name. Then, you can search for that subscription by name to update or remove it.

QuerySubscription<Task>(name: "long-running-completed") {
$0.completed == true && $0.progressMinutes > 120
}

If you do not specify a name for a subscription, you can search for the subscription by the query string.

QuerySubscription<Team> {
$0.teamName == "Developer Education"
}

Note

Duplicate subscriptions

Subscription names must be unique. Trying to append a subscription with the same name as an existing subscription throws an error.

If you do not explicitly name a subscription, and instead subscribe to the same unnamed query more than once, Realm does not persist duplicate queries to the subscription set.

If you subscribe to the same query more than once under different names, Realm persists both subscriptions to the subscription set.

Add a subscription in a subscriptions update block. You append each new subscription to the client's Realm subscriptions.

Tip

If your app accesses Realm in an async/await context, mark the code with @MainActor to avoid threading-related crashes.

let realm = try await getRealmWithSingleSubscription()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmWithSingleSubscription() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
try await subscriptions.update {
subscriptions.append(
QuerySubscription<Team> {
$0.teamName == "Developer Education"
})
}
return realm
}

You can add multiple subscriptions within a subscription update block, including subscriptions of different object types.

let realm = try await getRealmWithMultipleSubscriptions()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmWithMultipleSubscriptions() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
try await subscriptions.update {
subscriptions.append(
QuerySubscription<Task>(name: "completed-tasks") {
$0.completed == true
})
subscriptions.append(
QuerySubscription<Team> {
$0.teamName == "Developer Education"
})
}
return realm
}

New in version 10.28.0.

You must have at least one subscription before you can read from or write to the realm. You can bootstrap a realm with an initial subscription set when you open it with the flexibleSyncConfiguration(). Pass the initialSubscriptions parameter with the subscription queries you want to use to bootstrap the realm:

var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
subs.append(
QuerySubscription<Team> {
$0.teamName == "Developer Education"
})
})

If your app needs to rerun this initial subscription every time the app starts, you can pass an additional parameter - rerunOnOpen. This is a bool that denotes whether the initial subscription should re-run every time the app starts. You might need to do this to re-run dynamic time ranges or other queries that require a re-computation of static variables for the subscription.

In this example, we don't want users to be overwhelmed by irrelevant tasks, so we'll load only tasks due within the previous 7 days and the next 7 days. Tasks that were due more than a week ago are no longer relevant, and tasks that are due further out than the next week are also not relevant. With rerunOnOpen here, the query dynamically recalculates the relevant objects to sync based on the desired date range every time the app starts.

// Set the date a week ago and the date a week from now, as those are the dates we'll use
// in the Flexible Sync query. `rerunOnOpen` lets the app recalculate this query every
// time the app opens.
let secondsInAWeek: TimeInterval = 604800
let dateLastWeek = (Date.now - secondsInAWeek)
let dateNextWeek = (Date.now + secondsInAWeek)
var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
subs.append(
QuerySubscription<Task> {
$0.dueDate > dateLastWeek && $0.dueDate < dateNextWeek
})
}, rerunOnOpen: true)

In addition to syncing all objects that match a given query, you can subscribe to all objects of a specific type. You do this by appending a subscription without providing a query.

For example, if you don't want to see a specific team, but instead want to subscribe to all the Team objects, you could do this:

let realm = try await subscribeToObjectsOfAType()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func subscribeToObjectsOfAType() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
try await subscriptions.update {
subscriptions.append(QuerySubscription<Team>(name: "all_teams"))
}
XCTAssertEqual(subscriptions.count, 1) // :remove
return realm
}

If your application flow appends the same named subscription to the subscription set every time you run the application, this is disallowed. In this case, add a check for an existing subscription before appending it:

let realm = try await checkAndAddSubscription()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func checkAndAddSubscription() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
let foundSubscription = subscriptions.first(named: "user_team")
try await subscriptions.update {
if foundSubscription != nil {
foundSubscription!.updateQuery(toType: Team.self, where: {
$0.teamName == "Developer Education"
})
} else {
subscriptions.append(
QuerySubscription<Team>(name: "user_team") {
$0.teamName == "Developer Education"
})
}
}
return realm
}

Updating the subscription set locally is only one component of changing a subscription. After the local subscription change, the realm synchronizes with the server to resolve any updates to the data due to the subscription change. This could mean adding or removing data from the synced realm.

If your application does not use Swift's async/await feature, you can react to subscription changes syncing with the server using the onComplete block. This block is called after subscriptions are synchronized with the server. If you want to react to subscription state changes by redrawing a UI, for example, or taking another action based on changes to the data set, take those actions in onComplete. This is also where you can handle optional errors that occur during synchronization.

let subscriptions = realm.subscriptions
subscriptions.update({
subscriptions.append(
QuerySubscription<Task> {
$0.assignee == "John Doe"
})
}, onComplete: { error in // error is optional
if error == nil {
// Flexible Sync has updated data to match the subscription
} else {
// Handle the error
}
})

If your application uses async/await, you don't need the onComplete block. The update executes asynchronously and throws an error if the update cannot complete successfully.

@MainActor
func changeSubscription() async throws {
let subscriptions = realm.subscriptions
try await subcriptions.update {
subscriptions.remove {
QuerySubscription<Task> {
$0.assignee == "Joe Doe"
}
}
}
}

Tip

If your app accesses Realm in an async/await context, mark the code with @MainActor to avoid threading-related crashes.

Use the SubscriptionSet.state property to read the current state of the subscription set.

The superseded state is a SyncSubscriptionState that can occur when another thread updates a subscription on a different instance of the subscription set. If the state becomes superseded, you must obtain a new instance of the subscription set before you can update it.

Note

Subscription State "Complete"

The subscription set state "complete" does not mean "sync is done" or "all documents have been synced". "Complete" means the following two things have happened:

  • The subscription has become the active subscription set that is currently being synchronized with the server.

  • The documents that matched the subscription at the time the subscription was sent to the server are now on the local device. Note that this does not necessarily include all documents that currently match the subscription.

The Realm SDK does not provide a way to check whether all documents that match a subscription have synced to the device.

You can update a subscription's query using updateQuery. In this example, we search for a subscription matching our query and then update it with a new query.

let realm = try await getRealmWithUpdatedSubscriptions()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmWithUpdatedSubscriptions() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
try await subscriptions.update {
if let foundSubscription = subscriptions.first(ofType: Team.self, where: {
$0.teamName == "Developer Education"
}) {
foundSubscription.updateQuery(toType: Team.self, where: {
$0.teamName == "Documentation"
})
}
}
return realm
}

You can also search for a subscription by name . In this example, we search for a subscription query by name and then update it with a new query.

let realm = try await getRealmWithUpdatedSubscriptionName()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmWithUpdatedSubscriptionName() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
let foundSubscription = subscriptions.first(named: "user-team")
try await subscriptions.update {
foundSubscription?.updateQuery(toType: Team.self, where: {
$0.teamName == "Documentation"
})
}
return realm
}

To remove subscriptions, you can:

  • Remove a single subscription query

  • Remove all subscriptions to a specific object type

  • Remove all subscriptions

When you remove a subscription query, Realm asynchronously removes the synced data that matched the query from the client device.

You can remove a specific subscription query in a subscription update block using remove. Specify the query by name or use the query as a string to find the appropriate subscription query to remove.

let realm = try await getRealmAfterRemovingSubscription()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmAfterRemovingSubscription() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
// Look for a specific subscription, and then remove it
let foundSubscription = subscriptions.first(named: "docs-team")
try await subscriptions.update {
subscriptions.remove(foundSubscription!)
}
// Or remove a subscription that you know exists without querying for it
try await subscriptions.update {
subscriptions.remove(named: "existing-subscription")
}
return realm
}

If you want to remove all subscriptions to a specific object type, use the removeAll method with ofType in a subscription update block.

let realm = try await getRealmAfterRemovingAllSubscriptionsToAnObjectType()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmAfterRemovingAllSubscriptionsToAnObjectType() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
try await subscriptions.update {
subscriptions.removeAll(ofType: Team.self)
}
return realm
}

To remove all subscriptions from the subscription set, use the removeAll method in a subscription update block.

Important

If you remove all subscriptions and do not add a new one, you'll get an error. A realm opened with a flexible sync configuration needs at least one subscription to sync with the server.

let realm = try await getRealmAfterRemovingAllSubscriptions()
// Opening a realm and accessing it must be done from the same thread.
// Marking this function as `@MainActor` avoids threading-related issues.
@MainActor
func getRealmAfterRemovingAllSubscriptions() async throws -> Realm {
let realm = try await Realm(configuration: flexSyncConfig)
let subscriptions = realm.subscriptions
try await subscriptions.update {
subscriptions.removeAll()
}
return realm
}

Every write transaction for a subscription set has a performance cost. If you need to make multiple updates to a Realm object during a session, consider keeping edited objects in memory until all changes are complete. This improves sync performance by only writing the complete and updated object to your realm instead of every change.

Flexible Sync has some limitations when using RQL operators. When you write the query subscription that determines which data to sync, the server does not support these query operators. However, you can still use the full range of RQL features to query the synced data set in the client application.

Operator Type
Unsupported Operators
Aggregate Operators
@avg, @count, @max, @min, @sum
Query Suffixes
DISTINCT, SORT, LIMIT

Case insensitive queries ([c]) cannot use indexes effectively. As a result, case insensitive queries are not recommended, since they could lead to performance problems.

Flexible Sync only supports @count for array fields.

Flexible Sync supports querying lists using the IN operator.

You can query a list of constants to see if it contains the value of a queryable field:

// Query a constant list for a queryable field value
"priority IN { 1, 2, 3 }"

If a queryable field has an array value, you can query to see if it contains a constant value:

// Query an array-valued queryable field for a constant value
"'comedy' IN genres"

Warning

You cannot compare two lists with each other in a Flexible Sync query. Note that this is valid Realm Query Language syntax outside of Flexible Sync queries.

// Invalid Flexible Sync query. Do not do this!
"{'comedy', 'horror', 'suspense'} IN genres"
// Another invalid Flexible Sync query. Do not do this!
"ANY {'comedy', 'horror', 'suspense'} != ANY genres"

Flexible Sync does not support querying on properties in Embedded Objects or links. For example, obj1.field == "foo".

←  Configure & Open a Synced Realm - Swift SDKWrite Data to a Synced Realm - Swift SDK →
Share Feedback
© 2023 MongoDB, Inc.

About

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