I’m using Realm database with iOS and SwiftUI. I added one new field to one of my Realm object schema, let’s call it Project. Now when I do realm.objects(Project.self) it complaints that I need to do a migration. So as per the iOS migration doc, I added a schemaVersion and migrationBlock to my realm init code as follows.
config.schemaVersion = 1 // Older version was 0
config.migrationBlock = { migration, oldSchemaVersion in
logger.debug("Performing migration since old schema \(oldSchemaVersion) is behind current schema \(currentSchemaVersion) ...")
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: Project.className()) { (old, new) in
new!["isDeleted"] = false
}
})
}
I printed out config just before opening the Realm, and I can see that the config has a migration block field set.
I printed the Realm config again after I tried opening Realm as Realm.asyncOpen(configuration: config) and Realm(configuration: config), but the migration block is missing.
My migration block is not getting triggered and I suspect that I’m missing something! Thank you in advance for any help in the right direction
Edit
As per some Stack Overflow posts, I also tried using a bigger schema version (2, 5, 10 to be specific). Also, I initialize my main realm (which Project.self is a part of) when the SwiftUI.App is initialized.
I’ll provide some more context and code. First let me answer your 3 questions.
I think didFinishLaunchingWithOptions is a UIKit concept from AppDelegate. But I’m using SwiftUI so I’m not 100% sure what is the equivalent of it there. Correct me if I’m mistaken.
I’m using Realm Sync for premium users and local realms for free users. This can be seen in the code below.
I tried assigning Realm.Configuration.defaultConfiguration = config in my withRealmConfig() method below, but that had no effect on migration block whatsoever.
Full Code - I initialize a class called AppState as a singleton. This class handles user login and creation of user realm with partition as user=\(user.id). Since, it is a static singleton object, it will be instantiated at application start.
// AppState.swift
import Foundation
import RealmSwift
import Combine
class AppState : ObservableObject {
static let shared = AppState()
let app = RealmSwift.App(id: "tasktracker-abcd")
@Published var isPremiumUser = false
@Published var user: RealmSwift.User? = nil
@Published var userRealm: Realm? = nil
// Other fields and vars
private init() {
// Open realm when a user logs in
$user.compactMap { $0 }
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.flatMap { openRealm(for: "user=\($0.id)") }
.sink(receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
print("Unable to open realm due to error: \(error.localizedDescription)")
}
}, receiveValue: { [weak self] realm in
print("User realm opened successfully! Realm config: \(realm.configuration)")
self?.userRealm = realm
})
.store(in: &subscriptions)
}
}
And here’s my Realm initialization code. As described earlier, I use Sync’ed realm as well local realm based on whether the user is a premium user or not. Also, my custom config mainly changes the realm path to .../Documents/<user_id>/<user_partition>.realm instead of using the default one. Apart from that, it also adds the migration block and schema version.
// RealmService.swift
import Foundation
import RealmSwift
import Combine
func openRealm(for partition: String) -> AnyPublisher<Realm, Error> {
return Just(partition)
.filter { !$0.isEmpty }
.receive(on: DispatchQueue.main)
.compactMap(withRealmConfig(partitionValue:))
.flatMap(openCorrectRealmFlavor(with:))
.eraseToAnyPublisher()
}
private func withRealmConfig(partitionValue: String) -> Realm.Configuration? {
var config: Realm.Configuration
// Init realm config with or without sync
if AppState.shared.isPremiumUser {
config = user.configuration(partitionValue: partitionValue)
} else {
config = Realm.Configuration.defaultConfiguration
}
deletePathComponentsTillDocsDir(config.fileURL) // Reduces path to app containers "Documents" dir
config.fileURL?.appendPathComponent(user.id) // Creates a user dir with the user.id
// Create dir if not exists using FileManager
do {
try createRealmDirIfNotPresent(dir: config.fileURL!)
} catch let error {
logger.error("Error creating directory for realm: \(error.localizedDescription)")
return nil
}
// Set realm filename and extension
config.fileURL?.appendPathComponent(partitionValue.encoded())
config.fileURL?.appendPathExtension("realm")
// Perform migrations if required
config.schemaVersion = currentSchemaVersion
config.migrationBlock = { migration, oldSchemaVersion in
logger.debug("Performing migration since old schema \(oldSchemaVersion) is behind current schema \(currentSchemaVersion) ...")
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: Project.className()) { (old, new) in
new!["isDeleted"] = false
}
}
}
print("Realm config: \(config)")
return config
}
private func openCorrectRealmFlavor(with config: Realm.Configuration) -> AnyPublisher<Realm, Error> {
if AppState.shared.isPremiumUser {
print("Opening online sync'ed realm...")
return openSyncedRealm(with: config)
} else {
print("Opening local realm...")
return openLocalRealm(with: config)
}
}
private func openSyncedRealm(with config: Realm.Configuration) -> AnyPublisher<Realm, Error> {
return Realm.asyncOpen(configuration: config).eraseToAnyPublisher()
}
private func openLocalRealm(with config: Realm.Configuration) -> AnyPublisher<Realm, Error> {
return Result { try Realm(configuration: config) }.publisher.eraseToAnyPublisher()
}
The migration needs to start at app launch. This is untested but you can add a class to give your app the traditional appDelegate functionality like this
import SwiftUI
import RealmSwift
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Migration Code Goes here")
return true
}
}
@main
struct SwiftUI_TestApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
and then add your migration code in the AppDelegate. Again, that’s off the top of my head and 100% untested so if it doesn’t work, I will update. There are probably better options as well, but this was the first that came to mind considering the ObjC underpinnings of Realm.
I don’t think you can migrate sync’d realms - the file structure is different. It’s not clear if you are attempting to migrate a local realm as well as a sync’d realm though. I could be wrong on that point so please correct me if so
This should now work if the AppDelegate code works.
I’ll try point 1 and 3, out and let you know. Regarding point 2 - yes my migration covers sync’ed realms as well as local realms. The case that I tried was for a sync’ed realm. If the migration doesn’t work in sync’ed realms, what is the alternate option - populate the new fields manually in Atlas, delete the realm and reinitialize it?
Thank you for pointing out that migration blocks don’t work for sync’d realms. I tried my code for local realm and it triggers the migration block when schema version is changed.
It would be a good idea to have this behavior highlighted on the docs page as a gotcha! I hope someone from Realm team updates it.