Manage Flexible Sync Subscriptions - Swift SDK
On this page
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:
- Configure Flexible Sync on the backend
- Initialize the app
- Authenticate a user in your client project.
- Open the synced Realm with a Flexible Sync configuration
- Add subscriptions to the client application
You can add, update, and remove query subscriptions to determine which data syncs to the client device.
This page details how to manage subscriptions for Flexible Sync.
For general information about using Atlas 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.
Flexible Sync supports Combine.
Flexible Sync does not support all the operators available in Realm Query Language. See Flexible Sync RQL Limitations for details.
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> }
Subscribe to Queryable Fields
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.
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.
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" }
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.
let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append( QuerySubscription<Team> { $0.teamName == "Developer Education" }) }
You can add multiple subscriptions within a subscription update block, including subscriptions of different object types.
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" }) }
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 subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append(QuerySubscription<Team>(name: "all_teams")) }
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 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" }) } }
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" } } } }
SyncSubscriptionState Enum
Additionally, you can watch the state of the subscription set with the
SyncSubscriptionState
enum. You can use subscription state to:
- Show a progress indicator while data is downloading
- Find out when a subscription set becomes superseded
- Wait for a subscription that has been persisted locally to sync with the server
- Watch for errors
Superseded
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.
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 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" }) } }
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 subscriptions = realm.subscriptions let foundSubscription = subscriptions.first(named: "user-team") try await subscriptions.update { foundSubscription?.updateQuery(toType: Team.self, where: { $0.teamName == "Documentation" }) }
Remove Subscriptions
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.
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 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") }
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 subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll(ofType: Team.self) }
Remove All Subscriptions
To remove all subscriptions from the subscription set, use the removeAll
method in a subscription update block.
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 subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.removeAll() }
Flexible Sync RQL Limitations
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.
Unsupported Query Operators in Flexible Sync
Operator Type | Unsupported Operators |
---|---|
String Operators | in |
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.
Embedded or Linked Objects
Flexible Sync does not support querying on properties in Embedded Objects
or links. For example, obj1.field = “foo”
.