Building a Mobile Chat App Using Realm – The New and Easier Way
Rate this code example
In my last post, I walked through how to integrate Realm into a mobile chat app in . Since then, the Realm engineering team has been busy, and introduced new features that make the SDK way more "SwiftUI-native." For developers, that makes integrating Realm into SwiftUI views much simpler and more robust. This article steps through building the same chat app using these new features. Everything in still works, and it's the best starting point if you're building an app with UIKit rather than SwiftUI.
If you're looking to add a chat feature to your mobile app, you can repurpose the article's code and the associated repo. If not, treat it as a case study that explains the reasoning behind the data model and partitioning/syncing decisions taken. You'll likely need to make similar design choices in your apps.
Watch this demo of the app in action.
This article was updated in July 2021 to replace
@Persistedannotation that was introduced in Realm-Cocoa 10.10.0.
If you want to build and run the app for yourself, this is what you'll need:
- XCode 12.3+
- Realm-Swift 10.6+ (recommended to use the Swift Package Manager (SPM) rather than Cocoa Pods)
Name the app "RChat" and click "Create Application".
Copy the "App ID." You'll need to use this in your iOS app code:
Usernow conforms to Realm-Cocoa's
ObjectKeyIdentifiableprotocol, automatically adding identifiers to each instance that are used by SwiftUI (e.g., when iterating over results in a
ForEachloop). It's like
Identifiablebut integrated into Realm to handle events such as Atlas Device Sync adding a new object to a result set or list.
conversationsis now a
varrather than a
let, allowing us to append new items to the list.
AppStateclass is so much simpler now. Wherever possible, the opening of a Realm is now handled when opening the view that needs it.
As part of adopting the latest Realm-Cocoa SDK feature, I no longer need to store open Realms in
AppState(as Realms are now opened as part of loading the view that needs them).
userattribute to represent the user currently logged into the app (and Realm). If
useris set to
nil, then no user is logged in:
The app uses the to interact with the back end Atlas App Services application to perform actions such as logging into Realm. Those operations can take some time as they involve accessing resources over the internet, and so we don't want the app to sit busy-waiting for a response. Instead, we use publishers and subscribers to handle these events.
userRealmPublisherare publishers to handle logging in, logging out, and opening Realms for a user:
AppStateclass is instantiated, the actions are assigned to each of the Combine publishers:
We'll later see that an event is sent to
loginPublisherwhen a user has successfully logged in. In
AppState, we define what should be done when those events are received. Events received on
loginPublishertrigger the opening of a realm with the partition set to
user=<id of the user>, which in turn sends an event to
When the Realm has been opened and the Realm sent to
useris initialized with the
Userobject retrieved from the Realm. The user's presence is set to
After logging out of Realm, we simply set
After seeing what happens after a user has logged into Realm, we need to circle back and enable email/password authentication in the backend Atlas App Services app. Fortunately, it's straightforward to do.
From the Atlas UI, select "Authentication" from the lefthand menu, followed by "Authentication Providers." Click the "Edit" button for "Email/Password":
Enable the provider and select "Automatically confirm users" and "Run a password reset function." Select "New function" and save without making any edits:
Don't forget to click on "REVIEW & DEPLOY" whenever you've made a change to the backend Realm app.
Select "Triggers" and then click on "Add a Trigger":
Set the "Trigger Type" to "Authentication," provide a name, set the "Action Type" to "Create" (user registration), set the "Event Type" to "Function," and then select "New Function":
Name the function
createNewUserDocumentand add the code for the function:
Note that we set the
user=<id of the user>, which matches the partition used when the iOS app opens the User Realm.
"Save" then "REVIEW & DEPLOY."
Browse to the "Rules" section in the App Services UI and click on "Add Collection." Set "Database Name" to
RChatand "Collection Name" to
User. We won't be accessing the
Usercollection directly through App Services, so don't select a "Permissions Template." Click "Add Collection":
At this point, I'll stop reminding you to click "REVIEW & DEPLOY!"
Select "Schema," paste in this schema, and then click "SAVE":
Repeat for the
And for the
We use Atlas Device Sync to synchronize objects between instances of the iOS app (and we'll extend this app also to include Android). It also syncs those objects with Atlas collections. Note that there are three options to create a schema:
- Manually code the schema as a JSON schema document.
- Derive the schema from existing data stored in Atlas. (We don't yet have any data and so this isn't an option here.)
- Derive the schema from the Realm objects used in the mobile app.
We've already specified the schema and so will stick to the first option.
Select "Sync" and then select your Atlas cluster. Set the "Partition Key" to the
partitionattribute (it appears in the list as it's already in the schema for all three collections), and the rules for whether a user can sync with a given partition:
The "Read" rule controls whether a user can establish a one-way read-only sync relationship to the mobile app for a given user and partition. In this case, the rule delegates this to an Atlas Function named
The "Write" rule delegates to the
To create these functions, select "Functions" and click "Create New Function." Make sure you type the function name precisely, set "Authentication" to "System," and turn on the "Private" switch (which means it can't be called directly from external services such as our mobile app):
Create a Function named
userDocWrittenTo, set "Authentication" to "System," and make it private. This article is aiming to focus on the iOS app more than the backend app, and so we won't delve into this code:
Set up a database trigger to execute the new function whenever anything in the
This section is virtually unchanged. As part of using the new Realm SDK features, there is now less in
AppState(including fewer publishers), and so less attributes need to be set up as part of the login process.
We've now created enough of the backend app that mobile apps can now register new Realm users and use them to log into the app.
When first run, no user is logged in, and so
AppState.loggedInchecks whether a user is currently logged into the Realm
Clicking the button executes one of two functions:
signupmakes an asynchronous call to the Realm SDK to register the new user. Through a Combine pipeline,
signupreceives an event when the registration completes, which triggers it to invoke the
loginfunction uses the Realm SDK to log in the user asynchronously. If/when the Realm login succeeds, the Combine pipeline sends the Realm user to the
loginPublisherpublishers (recall that we've seen how those are handled within the
When the view loads, the UI is populated with any existing profile information found in the
Userobject in the
As the user updates the UI elements, the Realm
Userobject isn't changed. It's not until they click "Save User Profile" that we update the
state.useris an object that's being managed by Realm, and so it must be updated within a Realm transaction. Using one of the new Realm SDK features, the Realm for this user's partition is made available in
SetProfileViewby injecting it into the environment from
userRealmthrough the environment and uses it to create a transaction (line 10):
Once saved to the local Realm, Device Sync copies changes made to the
Userobject to the associated
Userdocument in Atlas.
Once the user has logged in and set up their profile information, they're presented with the
ConversationListView. Again, we use the new SDK feature to implicitly open the Realm for this user partition and pass it through the environment from
At any time, another user can include you in a new group conversation. This view needs to reflect those changes as they happen:
When the other user adds us to a conversation, our
Userdocument is updated automatically through the magic of Atlas Device Sync and our Atlas Trigger. Prior to Realm-Cocoa 10.6, we needed to observe the Realm and trick SwiftUI into refreshing the view when changes were received. The Realm/SwiftUI integration now refreshes the view automatically.
When you click in the new conversation button in
ConversationListView, a SwiftUI sheet is activated to host
NewConversationView. This time, we implicitly open and pass in the
ChatsterRealm (for the universal partition
NewConversationViewis similar to
SetProfileView.in that it lets the user provide a number of details which are then saved to Realm when the "Save" button is tapped.
In order to use the "Realm injection" approach, we now need to delegate the saving of the
Userobject to another view (
ChatsterRealm but the updated
Userobject needs be saved in a transaction for the
Something that we haven't covered yet is applying a filter to the live Realm search results. Here we filter on the
userNamewithin the Chatster objects:
When the status of a conversation changes (users go online/offline or new messages are received), the card displaying the conversation details should update.
conversation.unreadCountis part of the
Userobject, and so we need another Atlas Trigger to update that whenever a new chat message is posted to a conversation.
We add a new Atlas Function
chatMessageChangethat's configured as private and with "System" authentication (just like our other functions). This is the function code that will increment the
Userdocuments for members of the conversation:
That function should be invoked by a new database trigger (
ChatMessageChange) to fire whenever a document is inserted into the
has a lot of similarities with
ConversationListView, but with one fundamental difference. Each conversation/chat room has its own partition, and so when opening a conversation, you need to open a new Realm. Again, we use the new SDK feature to open and pass in the Realm for the appropriate conversation partition:
If you worked through , then you may have noticed that I had to introduce an extra view layer—
ChatRoomBubblesView—in order to open the Conversation Realm. This is because you can only pass in a single Realm through the environment, and
ChatRoomViewneeded the User Realm. On the plus side, we no longer need all of the boilerplate code to open the Realm from the view's
The Realm/SwiftUI integration means that the UI will automatically refresh whenever a new chat message is added to the Realm, but I also want to scroll to the bottom of the list so that the latest message is visible. We can achieve this by monitoring the Realm. Note that we only open a
ConversationRealm when the user opens the associated view because having too many realms open concurrently can exhaust resources. It's also important that we stop observing the Realm by setting it to
nilwhen leaving the view:
Note that we clear the notification token when leaving the view, ensuring that resources aren't wasted.
To send a message, all the app needs to do is to add the new chat message to Realm. Atlas Device Sync will then copy it to Atlas, where it is then synced to the other users. Note that we no longer need to explicitly open a Realm transaction to append the new chat message to the Realm that was received through the environment:
Since the release of , Realm-Swift 10.6 added new features that make working with Realm and SwiftUI simpler. Simply by passing the Realm configuration through the environment, the Realm is opened and made available to the view, and that view can go on to make updates without explicitly starting a transaction. This article has shown how those new features can be used to simplify your code. It has gone through the key steps you need to take when building a mobile app using Realm, including:
- Managing the user lifecycle: registering, authenticating, logging in, and logging out.
- Managing and storing user profile information.
- Adding objects to Realm.
- Performing searches on Realm data.
- Syncing data between your mobile apps and with MongoDB Atlas.
- Reacting to data changes synced from other devices.
- Adding some backend magic using Atlas Triggers and Functions.
We've skipped a lot of code and functionality in this article, and it's worth looking through the rest of the app to see how to use features such as these from a SwiftUI iOS app:
- Location data
- Camera and photo library
- Actions when minimizing your app