Manage Flexible Sync Subscriptions - Swift SDK
On this page
- Overview
- Subscriptions
- Subscribe to Object Types
- Permissions
- Manage Subscriptions in Your Client App
- About the Examples on This Page
- Subscribe to Queries
- Subscribe to a Query
- Subscribe to a Query with a Subscription Name
- Wait for a Query Subscription to Sync
- Unsubscribe from a Query
- Subscribe to an Actor-Confined Query
- Manually Manage Subscriptions
- Add a Subscription
- Wait for Subscription Changes to Sync
- Subscription Set State
- Update Subscriptions with a New Query
- Remove Subscriptions
- Performance Considerations
- API Efficiency
- Group Updates for Improved Performance
- Flexible Sync RQL Requirements and Limitations
- Indexed Queryable Fields Subscription Requirements
- Unsupported Query Operators in Flexible Sync
- List Queries
- Embedded or Linked Objects
- Query Size Limit
Overview
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:
Authenticate a user in your client project.
You can manually add, update, and remove subscriptions to determine which data syncs to the client device. In Realm Swift SDK version 10.43.0 and later, you can subscribe to queries instead of or in addition to manually managing subscriptions.
Tip
Flexible Sync supports Combine.
Subscriptions
When you configure Flexible Sync on the backend, you specify which fields your client application can query using subscriptions.
Each subscription corresponds to a query on queryable fields for a specific object type. See Queryable Fields in the App Services documentation for more information.
For each query subscription, Realm looks for data matching the query. Data matching the subscription, where the user has the appropriate permissions, syncs between clients and the backend application.
You can construct queries with the Realm Swift SDK query engine.
Subscribe to Object Types
Subscription sets are based on object type. You might have multiple subscriptions if you have many types of Realm objects. You can also have multiple subscriptions on the same object type.
However, note the following if you use relationships or asymmetric objects in your app:
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.
Asymmetric Objects
If your app uses Data Ingest to unidirectionally sync asymmetric objects, you cannot create subscriptions for those objects. If your app contains asymmetric objects and non-asymmetric objects in the same realm, you can add Flexible Sync subscription queries for the non-asymmetric objects.
Permissions
Subscriptions work hand-in-hand with permissions to determine what data to Sync to your client application. The client application only sees the subset of data that matches your subscriptions which also matches the permissions of the logged-in user.
This page details how to manage client subscriptions for Flexible Sync. For information about setting up permissions for Flexible Sync, see: Flexible Sync Rules & Permissions.
Manage Subscriptions in Your Client App
In the client application, you add, update, and remove subscriptions to specific queries on the queryable fields. This determines which data syncs to the client device.
You can:
Add subscriptions with an optional subscription name:
In Realm Swift SDK version 10.43.0 and later, you can use
.subscribe()
to subscribe to queryResults
. This automatically adds the subscription to the subscription set.Manually add a subscription to the subscription set with the
subscriptions
API. Use this API if you need more control over subscriptions for performance optimization or business logic reasons. See Performance Considerations for more information.
React to subscription state
Update subscriptions with new queries
Remove individual subscriptions or all subscriptions for an object type
About the Examples on This Page
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 { true) var _id: ObjectId (primaryKey: var taskName: String var assignee: String? var completed: Bool var progressMinutes: Int var dueDate: Date } class Team: Object { true) var _id: ObjectId (primaryKey: var teamName: String var tasks: List<Task> var members: List<String> }
The examples on this page also assume you have an authorized user and a Flexible Sync configuration:
let app = App(id: APPID) do { let credentials = emailPasswordCredentials(app: app) let user = try await app.login(credentials: credentials) var flexSyncConfig = user.flexibleSyncConfiguration() flexSyncConfig.objectTypes = [Task.self, Team.self] do { // Open the synced realm and manage Flexible Sync subscriptions } catch { print("Failed to open realm: \(error.localizedDescription)") // handle error } } catch { fatalError("Login failed: \(error.localizedDescription)") }
Subscribe to Queries
New in version 10.43.0.
To simplify subscription management, Realm Swift SDK version 10.43.0
adds APIs to subscribe and unsubscribe from a query's
Results
set. These APIs abstract away the details of manually adding
and removing subscriptions.
Important
The .subcribe() API is in Preview
The .subscribe()
and .unsubscribe()
APIs described here are
currently in Preview. These APIs may be subject to changes in the future.
Subscribe to a Query
With an authenticated user and a Flexible Sync configuration, you can open a synced realm and query for the objects you want to read and write. You can .subscribe() to that query to create a Flexible Sync subscription for objects matching the query:
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Task.self) .where { $0.progressMinutes >= 60 }.subscribe() // Go on to work with subscribed results
This creates an unnamed subscription and adds it to the MutableSubscriptionSet
,
similar to manually creating a subscription.
Subscribe to a Query with a Subscription Name
If your app works with multiple subscriptions, or if you want to update a subscription, you may want to add a name when you subscribe to a query.
You can later use this name to update a subscription's query, check for a subscription by name, or remove the query by name.
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Team.self) .where { $0.teamName == "Developer Education" } .subscribe(name: "team_developer_education") // Go on to work with subscribed results
Wait for a Query Subscription to Sync
When you subscribe to a query's Results
set, that set does not contain
objects until it syncs. If your app creates objects, you may not need to
download synced data before the user works with it. However, if your app
requires data from the server before the user can work with it, you can
specify that a subscription should waitForSync
:
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Team.self) .where { $0.members.contains("Bob Smith") } .subscribe( name: "bob_smith_teams", waitForSync: .onCreation) // After waiting for sync, the results set contains all the objects // that match the query - in our case, 1 print("The number of teams that have Bob Smith as a member is \(results.count)")
This option uses the RLMWaitForSyncMode
enum, whose cases are:
.onCreation: Wait to download matching objects when your app creates the subscription. Otherwise, return without waiting for new downloads. The app must have an internet connection the first time you add the subscription.
.always: Wait to download matching objects when
.subscribe()
is executed. The app must have an internet connection when.subscribe()
is executed..never: Never wait to download matching objects. The app needs an internet connection for the user to authenticate the first time the app launches, but can open offline on subsequent launches using cached credentials.
You can optionally specify a timeout
value of type TimeInterval.
Unsubscribe from a Query
You can unsubscribe from a query's Results
set using the
.unsubscribe() API:
let realm = try await Realm(configuration: flexSyncConfig) let results = try await realm.objects(Task.self).where { $0.completed == false }.subscribe() // Go on to work with subscribed results. // Later... results.unsubscribe()
This removes the subscription from the MutableSubscriptionSet
,
similar to manually removing a subscription.
A Results
set may still contain objects after calling .unsubscribe()
if another subscription exists that contains overlapping objects.
Calling .unsubscribe()
does not wait for objects to be removed from the
realm. There is no API to wait for .unsubscribe()
to sync with the server.
Subscribe to an Actor-Confined Query
You can subscribe to an actor-confined query on the MainActor:
let realm = try await Realm(configuration: flexSyncConfig, actor: MainActor.shared) let results = try await realm.objects(Team.self) .where { $0.teamName == "Developer Education" } .subscribe(name: "team_developer_education") // Go on to work with subscribed results
Or subscribe to a query on a custom actor:
let realm = try await Realm(configuration: flexSyncConfig, actor: CustomGlobalActor.shared) let results = try await realm.objects(Team.self) .where { $0.teamName == "Developer Education" } .subscribe(name: "team_developer_education") // Go on to work with subscribed results
For more information about actor-confined realms, refer to Use Realm with Actors - Swift SDK.
Manually Manage Subscriptions
You can use the subscriptions
API to manually 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.
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
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. 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. 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 }
Bootstrap the Realm with Initial Subscriptions
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)
Subscribe to All Objects of a Specific Type
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. 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 }
Check for Existing Subscriptions Before Adding a Subscription
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. 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 }
Wait for Subscription Changes to Sync
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.
Pre Async/Await
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 } })
Async/Await
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.
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.
Subscription Set State
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.
Update Subscriptions with a New Query
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. 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. 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 }
Remove Subscriptions
To remove subscriptions, you can:
Remove a single subscription query
Remove all subscriptions to a specific object type
Remove all unnamed subscriptions
Remove all subscriptions
When you remove a subscription query, Realm asynchronously removes the synced data that matched the query from the client device.
Remove a Single Subscription
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. 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 }
Remove All Subscriptions to an Object Type
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. 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 }
Remove All Unnamed Subscriptions
New in version 10.43.0.
You may want to remove unnamed subscriptions that are transient or dynamically generated, but leave named subscriptions in place.
You can remove all unnamed subscriptions from the subscription set by
setting unnamedOnly
to true
when you call the removeAll
method:
let realm = try await Realm(configuration: flexSyncConfig) // Add 2 subscriptions, one named and one unnamed. let results = try await realm.objects(Team.self).where { $0.teamName == "Developer Education" }.subscribe(name: "team_developer_education") let results2 = try await realm.objects(Task.self).where { $0.completed == false }.subscribe() // Later, remove only the unnamed one let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll(unnamedOnly: true) }
Remove All Subscriptions
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. func getRealmAfterRemovingAllSubscriptions() async throws -> Realm { let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll() } return realm }
Performance Considerations
API Efficiency
Adding several subscriptions with the .subscribe()
and .unsubscribe()
APIs described in the Subscribe to Queries section
is less efficient than performing batch updates when you manually
manage subscriptions. On every .subscribe()
, the Swift SDK opens a new
update block. For better performance adding multiple
subscriptions, use the subscriptions.update
API described in the
Manually Manage Subscriptions section.
Group Updates for Improved Performance
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 RQL Requirements and Limitations
Indexed Queryable Fields Subscription Requirements
Adding an indexed queryable field to
your App can improve performance for simple queries on data that is strongly
partitioned. For example, an app where queries strongly map data to a device,
store, or user, such as user_id == $0, “641374b03725038381d2e1fb”
, is
a good candidate for an indexed queryable field. However, an indexed
queryable field has specific requirements for use in a query subscription:
The indexed queryable field must be used in every subscription query. It cannot be missing from the query.
The indexed queryable field must use an
==
orIN
comparison against a constant at least once in the subscription query. For example,user_id == $0, "641374b03725038381d2e1fb"
orstore_id IN $0, {1,2,3}
.
You can optionally include an AND
comparison as long as the indexed
queryable field is directly compared against a constant using ==
or IN
at least once. For example, store_id IN {1,2,3} AND region=="Northeast"
or store_id == 1 AND (active_promotions < 5 OR num_employees < 10)
.
Invalid Flexible Sync queries on an indexed queryable field include queries where:
The indexed queryable field does not use
AND
with the rest of the query. For examplestore_id IN {1,2,3} OR region=="Northeast"
is invalid because it usesOR
instead ofAND
. Similarly,store_id == 1 AND active_promotions < 5 OR num_employees < 10
is invalid because theAND
only applies to the term next to it, not the entire query.The indexed queryable field is not used in an equality operator. For example
store_id > 2 AND region=="Northeast"
is invalid because it uses only the>
operator with the indexed queryable field and does not have an equality comparison.The query is missing the indexed queryable field entirely. For example,
region=="Northeast
ortruepredicate
are invalid because they do not contain the indexed queryable field.
Unsupported Query Operators in Flexible Sync
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.
List Queries
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"
Embedded or Linked Objects
Flexible Sync does not support querying on properties in Embedded Objects
or links. For example, obj1.field == "foo"
.
Query Size Limit
The size limit for any given query subscription in your subscription set is 256 kB. Exceeding this limit results in a LimitsExceeded Error.