When Realm crashes your iOS app on launch and how to fix it

When Realm crashes your iOS app on launch and how to fix it

Recently we had a bug in the Barstool Sports iOS app that was affecting a very tiny percentage of users (less than 1%) but it was still something we wanted to fix. The bug was causing the app to crash on launch. The cause of this crash was Realm.

Jump to our bug and solution

Here at Barstool we use Realm in all of our apps as a cache. Why Realm and not Core Data, or some other persistence solution? Because Realm has great support for SwiftUI, it is way more developer friendly, and git friendly. The product itself works great, and is built by a very talented team of devs. I get it, you're an iOS dev and you hate using any 3rd party dependencies, so do I. But we use Realm and it makes our apps better and our jobs easier, so we love it.

First things first, we never do any migrations; if we modify our schema we delete the Realm and create a new one. This simplifies a lot and helps us avoid a lot of headaches. We can pull this off because we treat Realm as a local cache and not like a database with important user data that can't be lost. If your user's data can't be retrieved from your server then Migrations should be taken very seriously to keep user data safe and uncorrupted.

Our issue

OK, with that preamble out of the way, let's get to the bug and how we fixed it. Realm Core had a bug that was fixed in this commit. This bug caused a small percentage of our user base to have a corrupted realm file and because of this the app would crash every time they launched it. Our app loads Realm when the app is launched and the app is architected in a way that without Realm, nothing works. The Realm team fixed the bug, but there was no way to uncorrupt our users' Realm file.

What are our options? Like I said, we use Realm as a cache and our server is the source of truth for all data in the app, so deleting the Realm file is not a big deal in our situation.

First I thought that we could create a special Test Flight for users impacted by this crash. We could create a build of the app that would delete their Realm file on launch, thus solving the cause of the crash, (the corrupt Realm file) and then they could re-download the Barstool Sports app from the App Store. This solution would solve the issue but only for a small group of users who reach out to our support team and ask for help. Most users don't do that and getting users setup with Test Flight is annoying for everyone involved. So that option is out.

We had to do something to make sure that the corrupt Realm file doesn't try to load, so the easiest solution is to rename the path to where the Realm file will be saved on disk. We configured Realm to open (and create) a Realm file at a new directory path and we also deleted the Realm at the old path. Deleting the old Realm file is not required for this fix to work but it is good practice to give users back that disk space.

What does this look like in practice? We decided to add a version number to our Realm directory path, so instead of com.barstoolsports.realm our new directory could be com.barstoolsports.v2023_01.realm. We use this path to set the Realm's defaultConfiguration like so,

Realm.Configuration.defaultConfiguration = Realm.Configuration(
    fileURL: fileURL,
    inMemoryIdentifier: inMemoryIdentifier,
    schemaVersion: schemaVersion + RealmSchemaVersion,
    migrationBlock: migrationBlock,
    deleteRealmIfMigrationNeeded: deleteRealmIfMigrationNeeded,
    shouldCompactOnLaunch: { totalBytes, usedBytes in
        let fiftyMB = 50 * 1024 * 1024
        return (totalBytes > fiftyMB) && (usedBytes < totalBytes / 2)
    }
)

fileURL is derived from the path we set. Setting up Realm is outside the scope of this article, but MongoDB has great documentation here. You can use that documentation to find out more information about the rest of the Configuration method we're using here.

To delete the unused Realm that is being replaced and any future unused Realms we used the following code,

private let previousRealmPaths = [
    "com.barstoolsports.realm",
]

This array can be added to any time a realmPath we're using gets deprecated. And to delete the Realm(s),

func deleteOldRealmFiles() { 
    // 1   
    Task.detached(priority: .background) {
        // 2
        previousRealmPaths.forEach {
            // 3
            let fileURL = realmFileURL($0)    
            
            // 4
            if FileManager.default.fileExists(atPath: $0) {
                // 5
                try? FileManager.default.removeItem(at: fileURL)
            }
        }
    }
}

func realmFileURL(_ fileName: String) -> URL {
    guard let appGroup = appGroup, let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
        return URL(fileURLWithPath: RLMRealmPathForFile(fileName), isDirectory: false)
    }

    return directory.appendingPathComponent(fileName)
}
  1. Using Swift concurrency we create a detached task with a .background priority. This is important because we don't want this delete operation to take any resources away from the main thread. This work is not a top priority for the app to function and it can be given the lowest priority level.
  2. Loop through each string from our previousRealmPaths array
  3. Given the array string, get a URL from our realmFileURL method.
  4. Using the default FileManager check if a file exists at the given path. We don't need to delete the Realm if it is already deleted or never existed.
  5. Finally, using the default FileManager we delete the Realm file. This operation can throw an error. You'll notice here that we use try? to avoid handling any errors thrown. Deleting the Realm is not a top priority; if the delete fails it will try again the next time the app is loaded so a failure here is not a big deal.

How did I know this bug was caused by Realm? The realm team does a great job populating the stack trace with info such as,

please_report_this_issue_in_github_realm_realm_core_v_12_13_0

After seeing this in my crash stack trace I opened an issue on their GitHub page. The Realm devs are fast to respond and help. This is another reason we love using their product.

I briefly mentioned migrations above and I think it is important for you to understand that accessing Realm after the schema has changed will also cause the app to crash if you do not properly implement a migration. It is also possible to delete the current Realm if a migration is needed but you do not have a need to perform the migration because your data's source of truth is located somewhere outside of your app. Implementing a migration is outside the scope of this article, but you can learn more directly from the Realm team here.

How do you like to persist data in your iOS apps? Realm, core data, SQLite? Or maybe something else completely unique? Find me on Twitter and let me know, @TomRads.