Search code examples
swiftrealmwrapperprimitiverealm-migration

Realm Swift Migration: String Wrapper List to String List


I am trying to migrate our Realm Swift schema from using a String wrapper class to the primitive String for collections. I am running into issues extracting the string values from the wrapper during migration.

Here is the wrapper class:

class StringDB: Object {
    @objc var stringVal: String = ""

    convenience init(string: String) {
        self.init()
        stringVal = string
    }
}

Then I have an example class with a List<StringDB> property:

class MyObjDB: Object {
    var emails: List<StringDB> = List<StringDB>()
    @objc dynamic var id: String = UUID().uuidString

    convenience init(_ emails: [StringDB]) {
        self.init()
        for email in emails {
            self.emails.append(objectsIn: emails)
        }
    }

    override static func primaryKey() -> String? {
        return "id"
    }
}

that I want to convert to a List<String>. Here is my migration code.

let config = Realm.Configuration(schemaVersion: latestSchemaVersion, migrationBlock: {
    migration, version in
    if version < 2 {
        migration.enumerateObjects(ofType: MyObjDB.className(), { old, new in
            guard let old = old, let new = new else { return }
            let oldEmails = old["emails"] as! List<StringDB>
            let newEmails = List<String>()
            
            for email in oldEmails {
                newEmails.append(email.stringVal)
            }
            new["emails"] = newEmails
        })
    }
})

However, the let oldEmails = old["emails"] as! List<StringDB> cast fails. I've tried also casting the collection to List<MigrationObject> and then casting the individual objects to StringDB but that cast fails as well.

I've found a workaround that may be satisfactory (I haven't confirmed yet), of converting the MigrationObject directly to string using coercion like "\(email)" and then running a regEx that will extract a desired substring from the garbage (StringDB {\n\tstringVal = [email protected];\n}), but I have no idea yet whether that will hold up in production, and I would prefer to work with something resembling a recommended way for doing this migration.


Solution

  • The Realm version is not shown in the question and there are a few typo's in the code. For older Realm versions, Lists should be defined thusly:

    let emails = RealmSwift.List<StringDB>() //need RealmSwift. to differentiate it from Swift List
    

    then newer versions should be this:

    @Persisted var emails = RealmSwift.List<StringDB>()
    

    Then this is a problem as it iterates over the emails array count times and appends the entire emails list over and over for every email in that list.

    for email in emails {
       self.emails.append(objectsIn: emails)
    }
    

    Also, when using older Realm versons with @Objc property types, they need to include dynamic. So on the StringDB object this

    @objc var stringVal: String = ""
    

    should be this

    @objc dynamic var stringVal: String = ""
    

    Lastly, the MyObjDB needs to have somewhere to put the new email list. You can't overwrite the old one as it's not the correct type. So add a property

    class MyObjDB: Object {
        var emails: List<StringDB> = List<StringDB>()
        let updatedEmailList = List<String>()
    

    Then to the question: See comments in code for the flow

    How about this:

    migration.enumerateObjects(ofType: MyObjDB.className()) { oldItem, newItem in
        //instantiate a List of the old email objects as migration objects
        let oldEmailList = oldItem!["emails"] as! List<MigrationObject>
        
        //we're going to populate a new List with the email strings
        var newEmailList = List<String>()
        
        //iterate over the old list, extracting the string from the old object as
        //  a string an inject it into a new List of Strings
        for oldEmailObject in oldEmailList {
            let oldEmail = oldEmailObject["stringVal"] as! String
            newEmailList.append(oldEmail)
        }
    
        //assign the new List of strings to a new emails property
        newItem!["updatedEmailList"] = newEmailList
     }