Using Realm Flexible Sync in Your App – an iOS Tutorial
Tags: mongodb, swift, mobile, database
Published: 2022-02-25 12:44:10

## Introduction

We recently announced the [release of the Realm Flexible Sync preview](https://www.mongodb.com/developer/article/realm-flexible-sync/)—an opportunity for developers to take it for a spin and give us feedback. That article provided an overview of the benefits of flexible sync and how it works. TL;DR: You typically don't want to sync the entire backend database to every device—whether for capacity or security concerns. Realm Flexible Sync lets the developer provide queries to control exactly what the mobile app asks to sync, together with backend rules to ensure users can only access the data that they're entitled to.

This post builds on that introduction by showing how to add flexible sync to the RChat mobile app. I'll show how to configure the Realm backend app, and then what code needs adding to the mobile app.

Everything you see in this tutorial can be found in the [flex-sync branch of the RChat repo](https://github.com/realm/RChat/tree/flex-sync).

## Prerequisites

- Xcode 13.2+
- iOS 15+
- [Realm-Swift 10.22.0+](https://github.com/realm/realm-swift/releases)
- MongoDB 5.0+

## The RChat App

RChat is a messaging app. Users can add other users to a chat room and then share messages, images, and location with each other.

![Screen capture video of running the RCha App on 2 different iOS simulators. Two users join a chat room and send messages to each other](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/chat_realm_flex_sync_468d197c99.gif)

All of the user and chat message data is shared between instances of the app via Realm Sync.

There's a common Realm backend app. There are frontend apps for iOS and Android. This post focuses on the backend and the iOS app.

## Configuring the Realm Backend App

The backend app contains a lot of functionality that isn't connected to the sync functionality, and so I won't cover that here. If you're interested, then check out the [original RChat series](https://www.mongodb.com/developer/how-to/realm-swiftui-ios-chat-app/).

As a starting point, you can install the app. I'll then explain the parts connected to Realm Sync.

### Import the Backend Realm App

1. If you don't already have one, [create a MongoDB Atlas Cluster](https://cloud.mongodb.com/), keeping the default name of Cluster0. The Atlas cluster must be running MongoDB 5.0 or later.
2. Install the [Realm CLI](https://docs.mongodb.com/realm/deploy/realm-cli-reference) and [create an API key pair](https://docs.atlas.mongodb.com/configure-api-access#programmatic-api-keys).
3. Download the repo and install the Realm app:

```bash
git clone https://github.com/ClusterDB/RChat.git
git checkout flex-sync
cd RChat/RChat-Realm/RChat
realm-cli login --api-key <your new public key> --private-api-key <your new private key>
realm-cli import # Then answer prompts, naming the app RChat

```

4. From the Atlas UI, click on the Realm logo and you will see the RChat app. Open it and copy the App Id. You'll need to use this before building the iOS app.

![Screen capture of how to copy the Realm App ID in the Realm UI](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/copy_realm_app_id_b2d9b27ddc.png)

### How Flexible Sync is Enabled in the Back End
#### Schema

The schema represents how the data will be stored in MongoDB Atlas **and*- what the Swift (and Kotlin) model classes must contain. 

Each collection/class requires a schema. If you enable Realm's "Developer Mode" option, then Realm will automatically define the schema based on your Swift or Kotlin model classes. In this case, your imported Realm `App` includes the schemas, and so developer mode isn't needed. You can view the schemas by browsing to the "Schema" section in the [Realm UI](https://cloud.mongodb.com):

![Screen capture of schema section of the Realm UI](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/schemas_in_realm_ui_4032fe478c.png)

You can find more details about the schema/model in [Building a Mobile Chat App Using Realm – Data Architecture](https://www.mongodb.com/developer/how-to/realm-swiftui-ios-chat-app/), but note that for flexible sync (as opposed to the original partition-based sync), the `partition` field has been removed.

We're interested in the schema for three collections/model-classes:

**User:**

```json
{
  "bsonType": "object",
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "conversations": {
      "bsonType": "array",
      "items": {
        "bsonType": "object",
        "properties": {
          "displayName": {
            "bsonType": "string"
          },
          "id": {
            "bsonType": "string"
          },
          "members": {
            "bsonType": "array",
            "items": {
              "bsonType": "object",
              "properties": {
                "membershipStatus": {
                  "bsonType": "string"
                },
                "userName": {
                  "bsonType": "string"
                }
              },
              "required": [
                "membershipStatus",
                "userName"
              ],
              "title": "Member"
            }
          },
          "unreadCount": {
            "bsonType": "long"
          }
        },
        "required": [
          "unreadCount",
          "id",
          "displayName"
        ],
        "title": "Conversation"
      }
    },
    "lastSeenAt": {
      "bsonType": "date"
    },
    "presence": {
      "bsonType": "string"
    },
    "userName": {
      "bsonType": "string"
    },
    "userPreferences": {
      "bsonType": "object",
      "properties": {
        "avatarImage": {
          "bsonType": "object",
          "properties": {
            "_id": {
              "bsonType": "string"
            },
            "date": {
              "bsonType": "date"
            },
            "picture": {
              "bsonType": "binData"
            },
            "thumbNail": {
              "bsonType": "binData"
            }
          },
          "required": [
            "_id",
            "date"
          ],
          "title": "Photo"
        },
        "displayName": {
          "bsonType": "string"
        }
      },
      "required": [],
      "title": "UserPreferences"
    }
  },
  "required": [
    "_id",
    "userName",
    "presence"
  ],
  "title": "User"
}
```

`User` documents/objects represent users of the app.

**Chatster:**
```json
{
  "bsonType": "object",
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "avatarImage": {
      "bsonType": "object",
      "properties": {
        "_id": {
          "bsonType": "string"
        },
        "date": {
          "bsonType": "date"
        },
        "picture": {
          "bsonType": "binData"
        },
        "thumbNail": {
          "bsonType": "binData"
        }
      },
      "required": [
        "_id",
        "date"
      ],
      "title": "Photo"
    },
    "displayName": {
      "bsonType": "string"
    },
    "lastSeenAt": {
      "bsonType": "date"
    },
    "presence": {
      "bsonType": "string"
    },
    "userName": {
      "bsonType": "string"
    }
  },
  "required": [
    "_id",
    "presence",
    "userName"
  ],
  "title": "Chatster"
}
```

`Chatster` documents/objects represent a read-only subset of instances of `User` documents. `Chatster` is needed because there's a subset of `User` data that we want to make accessible to all users. E.g., I want everyone to be able to see my username, presence status, and avatar image, but I don't want them to see which chat rooms I'm a member of. 

Realm Sync lets you control which users can sync which documents, but it doesn't let you sync just a subset of a document's fields. That's why `Chatster` is needed. I'm looking forward to when Realm Sync permissions allow me to control access on a per-field (rather than per-document/class) basis. At that point, I can remove `Chatster` from the app.

**ChatMessage:**
```json
{
  "bsonType": "object",
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "author": {
      "bsonType": "string"
    },
    "authorID": {
      "bsonType": "string"
    },
    "conversationID": {
      "bsonType": "string"
    },
    "image": {
      "bsonType": "object",
      "properties": {
        "_id": {
          "bsonType": "string"
        },
        "date": {
          "bsonType": "date"
        },
        "picture": {
          "bsonType": "binData"
        },
        "thumbNail": {
          "bsonType": "binData"
        }
      },
      "required": [
        "_id",
        "date"
      ],
      "title": "Photo"
    },
    "location": {
      "bsonType": "array",
      "items": {
        "bsonType": "double"
      }
    },
    "text": {
      "bsonType": "string"
    },
    "timestamp": {
      "bsonType": "date"
    }
  },
  "required": [
    "_id",
    "authorID",
    "conversationID",
    "text",
    "timestamp"
  ],
  "title": "ChatMessage"
}
```
There's a `ChatMessage` document object for every message sent to any chat room.

#### Flexible Sync Configuration
You can view and edit the sync configuration by browsing to the "Sync" section of the Realm UI:

![Enabling Realm Flexible Sync in the Realm UI](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/configure_flex_sync_9b3221e283.png)

For this deployment, I've selected the Atlas cluster to use. **That cluster must be running MongoDB 5.0 or later**. At the time of writing, MongoDB 5.0 isn't available for shared clusters (including free-tier M0 instances)—that's expected to change very soon, possibly by the time that you're reading this.

You must specify which fields the mobile app can use in its sync filter queries. Without this, you can't refer to those fields in your sync queries or permissions. You are currently limited to 10 fields.

Scrolling down, you can see the sync permissions:

![Screenshot of a JSON document representing sync permissions in the Realm UI](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/sync_permission_f780c57241.png)

The UI has flattened the permissions JSON document; here's a version that's easier to read:

```json
{
   "rules": {
       "User": [
           {
               "name": "anyone",
               "applyWhen": {},
               "read": {
                   "_id": "%%user.id"
               },
               "write": {
                   "_id": "%%user.id"
               }
           }
       ],
       "Chatster": [
           {
               "name": "anyone",
               "applyWhen": {},
               "read": true,
               "write": false
           }
       ],
       "ChatMessage": [
           {
               "name": "anyone",
               "applyWhen": {},
               "read": true,
               "write": {
                   "authorID": "%%user.id"
               }
           }
       ]
   },
   "defaultRoles": [
       {
           "name": "all",
           "applyWhen": {},
           "read": {},
           "write": {}
       }
   ]
}
```

The `rules` component contains a sub-document for each of our collections. Each of those sub-documents contain an array of roles. Each role contains:

- The `name` of the role, this should be something that helps other developers understand the purpose of the role (e.g., "admin," "owner," "guest").
- `applyWhen`, which defines whether the requesting user matches the role or not. Each of our collections have a single role, and so `applyWhen` is set to `{}`, which always evaluates to true.
- A read rule—how to decide whether this user can view a given document. This is where our three collections impose different rules:
    - A user can read and write to their own `User` object. No one else can read or write to it.
    - Anyone can read any `Chatster` document, but no one can write to them. Note that these documents are maintained by database triggers to keep them consistent with their associated `User` document.
    - The author of a `ChatMessage` is allowed to write to it. Anyone can read any `ChatMessage`. Ideally, we'd restrict it to just members of the chat room, but permissions don't currently support arrays—this is another feature that I'm keen to see added.

## Adding Realm Flexible Sync to the iOS App

As with the back end, the iOS app is too big to cover in its entirety in this post. I'll explain how to build and run the app and then go through the components relevant to Realm Flexible Sync.

### Configure, Build, and Run the RChat iOS App

You've already downloaded the repo containing the iOS app, but you need to change directory before opening and running the app:

```bash
cd ../../RChat-iOS
pod install
open RChat.xcodeproj
```

Update `RChatApp.swift` with your Realm App Id (you copied that from the Realm UI when configuring your backend Realm app). In Xcode, select your device or simulator before building and running the app (⌘R). Select a second device or simulator and run the app a second time (⌘R).

On each device, provide a username and password and select the "Register new user" checkbox:
![iOS screenshot of registering a new user through the RChat app](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/register_rchat_user_f04514f5bf.png)

Once registered and logged in on both devices, you can create a new chat room, invite your second user, and start sharing messages and photos. To share location, you first need to enable it in the app's settings.

### Key Pieces of the iOS App Code
#### The Model

You've seen the schemas that were defined for the "User," "Chatster," and "ChatMessage" collections in the back end Realm app. Each of those collections has an associated Realm `Object` class in the iOS app. Sub-documents map to embedded objects that conform to `RealmEmbeddedObject`:

![UML diagram showing the User, Chatster, and ChatMessage classes—together with their embedded classes](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/R_Chat_Flex_Sync_Class_Model_496e297d36.png)

Let's take a close look at each of these classes:

**User Class**

![UML diagram showing the User class—together with its embedded classes](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/User_Flex_Sync_Class_Model_78340465be.png)

``` swift
class User: Object, ObjectKeyIdentifiable {
   @Persisted(primaryKey: true) var _id = UUID().uuidString
   @Persisted var userName = ""
   @Persisted var userPreferences: UserPreferences?
   @Persisted var lastSeenAt: Date?
   @Persisted var conversations = List<Conversation>()
   @Persisted var presence = "On-Line"
}

class UserPreferences: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var displayName: String?
   @Persisted var avatarImage: Photo?
}

class Photo: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var _id = UUID().uuidString
   @Persisted var thumbNail: Data?
   @Persisted var picture: Data?
   @Persisted var date = Date()
}

class Conversation: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var id = UUID().uuidString
   @Persisted var displayName = ""
   @Persisted var unreadCount = 0
   @Persisted var members = List<Member>()
}

class Member: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var userName = ""
   @Persisted var membershipStatus = "User added, but invite pending"
}
```

**Chatster Class**

![UML diagram showing the Chatster class—together with its embedded class](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/Chatster_Flex_Sync_Class_Model_7f5cf9bc28.png)

```swift
class Chatster: Object, ObjectKeyIdentifiable {
   @Persisted(primaryKey: true) var _id = UUID().uuidString // This will match the _id of the associated User
   @Persisted var userName = ""
   @Persisted var displayName: String?
   @Persisted var avatarImage: Photo?
   @Persisted var lastSeenAt: Date?
   @Persisted var presence = "Off-Line"
}

class Photo: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var _id = UUID().uuidString
   @Persisted var thumbNail: Data?
   @Persisted var picture: Data?
   @Persisted var date = Date()
}
```

**ChatMessage Class**

![UML diagram showing the ChatMessage class—together with its embedded class](https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/Chat_Message_Flex_Sync_Class_Model_17_25_06_c33478d3e9.png)

```swift
class ChatMessage: Object, ObjectKeyIdentifiable {
   @Persisted(primaryKey: true) var _id = UUID().uuidString
   @Persisted var conversationID = ""
   @Persisted var author: String? // username
   @Persisted var authorID: String
   @Persisted var text = ""
   @Persisted var image: Photo?
   @Persisted var location = List<Double>()
   @Persisted var timestamp = Date()
}

class Photo: EmbeddedObject, ObjectKeyIdentifiable {
   @Persisted var _id = UUID().uuidString
   @Persisted var thumbNail: Data?
   @Persisted var picture: Data?
   @Persisted var date = Date()
}
```

#### Accessing Synced Realm Data

Any iOS app that wants to sync Realm data needs to create a Realm `App` instance, providing the Realm App ID so that the Realm SDK can connect to the backend Realm app:

```swift
let app = RealmSwift.App(id: "rchat-xxxxx") // TODO: Set the Realm application ID
```

When a SwiftUI view (in this case, `LoggedInView`) needs to access synced data, the parent view must flag that flexible sync will be used. It does this by passing the Realm configuration through the SwiftUI environment:

```swift
LoggedInView(userID: $userID)
   .environment(\.realmConfiguration,
               app.currentUser!.flexibleSyncConfiguration())
```

`LoggedInView` can then access two variables from the SwiftUI environment:

```swift
struct LoggedInView: View {
   ...
   @Environment(\.realm) var realm
   @ObservedResults(User.self) var users
```

The users variable is a live query containing all synced `User` objects in the Realm. But at this point, no `User` documents have been synced because we haven't subscribed to anything.

That's easy to fix. We create a new function (`setSubscription`) that's invoked when the view is opened:

```swift
struct LoggedInView: View {
   ...
   @Binding var userID: String?
   ...
   var body: some View {
       ZStack {
           ...
       }
       .onAppear(perform: setSubscription)
   }

   private func setSubscription() {
       let subscriptions = realm.subscriptions
       subscriptions.write {
           if let currentSubscription = subscriptions.first(named: "user_id") {
               print("Replacing subscription for user_id")
               currentSubscription.update(toType: User.self) { user in
                   user._id == userID!
               }
           } else {
               print("Appending subscription for user_id")
               subscriptions.append(QuerySubscription<User>(name: "user_id") { user in
                   user._id == userID!
               })
           }
       }
   }
}
```

Subscriptions are given a name to make them easier to work with. I named this one `user_id`. 

The function checks whether there's already a subscription named `user_id`. If there is, then the function replaces it. If not, then it adds the new subscription. In either case, the subscription is defined by passing in a query that finds any `User` documents/objects where the `_id` field matches the current user's ID.

The subscription should sync exactly one `User` object to the realm, and so the code for the view's body can work with the `first` object in the results:

```swift
struct LoggedInView: View {
   ...
   @ObservedResults(User.self) var users
   @Binding var userID: String?
   ...
   var body: some View {
       ZStack {
           if let user = users.first {
               ...
               ConversationListView(user: user)
               ...
           }
       }
       .navigationBarTitle("Chats", displayMode: .inline)
       .onAppear(perform: setSubscription)
   }
}
```

Other views work with different model classes and sync queries. For example, when the user clicks on a chat room, a new view is opened that displays all of the `ChatMessage`s for that conversation:

```swift
struct ChatRoomBubblesView: View {
   ...
   @ObservedResults(ChatMessage.self, sortDescriptor: SortDescriptor(keyPath: "timestamp", ascending: true)) var chats
   @Environment(\.realm) var realm
   ...
   var conversation: Conversation?
   ...
   var body: some View {
       VStack {
           ...
       }
       .onAppear { loadChatRoom() }
   }

   private func loadChatRoom() {
       ...
       setSubscription()
       ...
   }

   private func setSubscription() {
       let subscriptions = realm.subscriptions
       subscriptions.write {
           if let conversation = conversation {
               if let currentSubscription = subscriptions.first(named: "conversation") {
                   currentSubscription.update(toType: ChatMessage.self) { chatMessage in
                       chatMessage.conversationID == conversation.id
                   }
               } else {
                   subscriptions.append(QuerySubscription<ChatMessage>(name: "conversation") { chatMessage in
                       chatMessage.conversationID == conversation.id
                   })
               }
           }
       }
   }
}
```

In this case, the query syncs all `ChatMessage` objects where the `conversationID` matches the `id` of the `Conversation` object passed to the view.

The view's body can then iterate over all of the matching, synced objects:

```swift
struct ChatRoomBubblesView: View {
...
   @ObservedResults(ChatMessage.self,
       sortDescriptor: SortDescriptor(keyPath: "timestamp", ascending: true)) var chats
   ...
   var body: some View {
       ...
       ForEach(chats) { chatMessage in
           ChatBubbleView(chatMessage: chatMessage,
                           authorName: chatMessage.author != user.userName ? chatMessage.author : nil,
                           isPreview: isPreview)
       }
       ...
   }
}
```

As it stands, there's some annoying behavior. If you open conversation A, go back, and then open conversation B, you'll initially see all of the messages from conversation A. The reason is that it takes a short time for the updated subscription to replace the `ChatMessage` objects in the synced Realm. I solve that by explicitly removing the subscription (which purges the synced objects) when closing the view:

```swift
struct ChatRoomBubblesView: View {
   ...
   @Environment(\.realm) var realm
   ...
   var body: some View {
       VStack {
           ...
       }
       .onDisappear { closeChatRoom() }
   }

   private func closeChatRoom() {
       clearSubscription()
       ...
   }

   private func clearSubscription() {
       print("Leaving room, clearing subscription")
       let subscriptions = realm.subscriptions
       subscriptions.write {
           subscriptions.remove(named: "conversation")
       }
   }
}
```

I made a design decision that I'd use the same name ("conversation") for this view, regardless of which conversation/chat room it's working with. An alternative would be to create a unique subscription whenever a new chat room is opened (including the ID of the conversation in the name). I could then avoid removing the subscription when navigating away from a chat room.  This second approach would come with two advantages: 

1. The app should be more responsive when navigating between chat rooms (if you'd previously visited the chat room that you're opening).
2. You can switch between chat rooms even when the device isn't connected to the internet.

The disadvantages of this approach would be:

1. The app could end up with a lot of subscriptions (and there's a cost to them).
2. The app continues to store all of the messages from any chat room that you've ever visited from this device. That consumes extra device storage and network bandwidth as messages from all of those rooms continue to be synced to the app.

A third approach would be to stick with a single subscription (named "conversations") that matches every `ChatMessage` object. The view would then need to apply a filter on the resulting `ChatMessage` objects so it only displayed those for the open chat room. This has the same advantages as the second approach, but can consume even more storage as the device will contain messages from all chat rooms—including those that the user has never visited.

Note that a different user can log into the app from the same device. You don't want that user to be greeted with someone else's data. To avoid that, the app removes all subscriptions when a user logs out:

```swift
struct LogoutButton: View {
   ...
   @Environment(\.realm) var realm


   var body: some View {
       Button("Log Out") { isConfirming = true }
       .confirmationDialog("Are you that you want to logout",
                           isPresented: $isConfirming) {
           Button("Confirm Logout", role: .destructive, action: logout)
           Button("Cancel", role: .cancel) {}
       }
       .disabled(state.shouldIndicateActivity)
   }

   private func logout() {
       ...
       clearSubscriptions()
       ...
   }

   private func clearSubscriptions() {
       let subscriptions = realm.subscriptions
       subscriptions.write {
           subscriptions.removeAll()
       }
   }
}
```
## Conclusion

In this article, you've seen how to include Realm Flexible Sync in your mobile app. I've shown the code for Swift, but the approach would be the same when building apps with Kotlin, Javascript, or .NET.

This is a preview release and we want your feedback.

Realm Flexible Sync will evolve to include more query and permission operators. Up next, we're looking to expose array operators (that would allow me to add tighter restrictions on who can ask to read which chat messages). We'll also enable querying on embedded documents.

Another feature I'd like to see is to limit which fields from a document get synced to a given user. This could allow the removal of the `Chatster` collection, as it's only there to provide a read-only view of a subset of `User` fields to other users.

Want to suggest an enhancement or up-vote an existing request? The most effective way is through our [feedback portal](https://feedback.mongodb.com/forums/923521-realm).

Got questions? Ask them in our [Community forum](https://www.mongodb.com/community/forums/c/realm/realm-sync/111).