← Blog

Updating Schema of Synced Realm and iOS App in Production

iOS
Realm
published on:
📚  This article is part of a series on 
iOS
 and 
Realm
:

Realm offers a development mode that allows you to conveniently design your app’s data models from within its source code. Once your app enters the production environment, you turn off the development mode as recommended. However, this leaves you in an unfortunate situation where updating your app’s data models becomes a little more complicated. The work required to perform such an update of the data model schema depends on the nature of the change to apply. The documentation of Realm classifies different types of changes according to their severance on the client and server side, respectively. In this article, I focus on a schema update that adds a required property to an existing object type.

After launching catalyst, a native iOS client for GitLab, I continued to fix bugs and develop new features. Eventually, this led me to an inevitable schema update that adds a new required property to an existing object type. Since this new property should be persisted in the database, the schema update would affect the client and the server side, i.e., the iOS app and Realm. Luckily, both of these changes are non-breaking according to the documentation.

Updating schema in Realm

As an example, I demonstrate the process of allowing users to pin a repository in catalyst to the list of repositories for faster access. We start by adding the required property isPinned to the schema of Project on the server side:


_13
{
_13
"title": "Project",
_13
"bsonType": "object",
_13
"required": ["isPinned"],
_13
"properties": {
_13
"_id": {
_13
"bsonType": "uuid"
_13
},
_13
"isPinned": {
_13
"bsonType": "bool"
_13
}
_13
}
_13
}

Since this modification is a non-breaking change, it does not require modifications on the client side, i.e., the iOS app’s source code. However, on the server side, we need to update existing documents. I choose functions to accomplish the update. In particular, I create two functions: one to update existing documents and another to update new documents. The latter is necessary since catalyst will not be aware of the new required property without an update. To update existing documents, I manually execute the following function once:


_13
exports = async function () {
_13
const projects = context.services
_13
.get("catalyst-production")
_13
.db("catalyst")
_13
.collection("Project");
_13
_13
const userProjects = await projects.updateMany(
_13
{},
_13
{ $set: { isPinned: false } }
_13
);
_13
_13
return null;
_13
};

For newly created documents, I use a function combined with a database trigger that goes off on insertions, i.e., when a new document is created:


_17
exports = async function (changeEvent) {
_17
const documentID = changeEvent.documentKey._id;
_17
const projects = context.services
_17
.get("catalyst-production")
_17
.db("catalyst")
_17
.collection("Project");
_17
_17
const document = await projects.findOne({ _id: documentID });
_17
if (document.isPinned === undefined) {
_17
await projects.updateOne(
_17
{ _id: documentID },
_17
{ $set: { isPinned: false } }
_17
);
_17
}
_17
_17
return null;
_17
};

Updating schema in iOS

After taking care of the server side, I also need to update the client side to be able to use the isPinned property. For this, I add the property to the object model and initialize the variable with a value of false since we do not want projects to be pinned by default:

Project.swift

_10
class Project: Object, Identifiable {
_10
// ...
_10
@Persisted var isPinned: Bool = false
_10
// ...
_10
}

As opposed to the server-side change, the client requires a migration. It should be noted that the official tutorial for Migrating Your iOS App's Synced Realm Schema in Production claims that such a migration would not be necessary since “Realm can automatically update the local realm to include this new attribute and initialize it to false”. However, doing so crashes catalyst and prompts me with the following error message:


RealmSwift/SwiftUI.swift:1481: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'Project.isPinned' has been added." UserInfo={NSLocalizedDescription=Migration is required due to the following errors:
- Property 'Project.isPinned' has been added., Error Code=10}

This makes sense given that the documentation on Realm’s Swift SDK notes that “Realm Database does not automatically set values for new required properties. You must use a migration block to set default values for new required properties.” Although the migration itself happens automatically, you have to specify a new schemaVersion in Realm’s default configuration, i.e., Realm.Configuration.defaultConfiguration, at least.

With the client and server side updated, catalyst can now act on the isPinned property and store the corresponding documents in Realm. Meanwhile, older versions of catalyst can still interact with Realm despite not being aware of the newly added required property.