liman.io

liman.io

The logo of Realm by MongoDB on a dark background

Updating Schema of Synced Realm and iOS App in Production

Realm

published on 2022-11-20

📚  This article is part of a series on Realm:
  • Tackling `AttributeGraph precondition failure: setting value during update` using Realm in SwiftUI
  • Updating Schema of Synced Realm and iOS App in Production
  • Synced Realm on iOS with SwiftUI using Sign-in with Apple for Authentication

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 gitlapp, 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 gitlapp 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:

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

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 gitlapp will not be aware of the new required property without an update. To update existing documents, I manually execute the following function once:

exports = async function () {
  const projects = context.services
    .get("gitlapp-production")
    .db("gitlapp")
    .collection("Project");

  const userProjects = await projects.updateMany(
    {},
    { $set: { isPinned: false } }
  );

  return null;
};

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:

exports = async function (changeEvent) {
  const documentID = changeEvent.documentKey._id;
  const projects = context.services
    .get("gitlapp-production")
    .db("gitlapp")
    .collection("Project");

  const document = await projects.findOne({ _id: documentID });
  if (document.isPinned === undefined) {
    await projects.updateOne(
      { _id: documentID },
      { $set: { isPinned: false } }
    );
  }

  return null;
};

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
class Project: Object, Identifiable {
    // ...
    @Persisted var isPinned: Bool = false
    // ...
}

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 gitlapp 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, gitlapp can now act on the isPinned property and store the corresponding documents in Realm. Meanwhile, older versions of gitlapp can still interact with Realm despite not being aware of the newly added required property.

Daniel Fürst © 2023 • Legal Notice • Privacy Policy