Search code examples
swiftrealmrealm-migration

Realm (Swift): How to get at MutableSet data during migration?


I've got a local Realm (using the RealmSwift API version 10.15.1) that has a Player object that I'm trying to migrate. The Player currently contains field called preferredPositions that is a MutableSet<PositionClass>. The definition of Player looks like this:

@objc final class Player: Object {
    @Persisted(primaryKey: true) var playerId: ObjectId
    @Persisted var name: String = ""
    @Persisted var preferredPositions: MutableSet<PositionClass> = MutableSet<PositionClass>()
    ...
}

and PositionClass looks like this:

class PositionClass: Object {
    @Persisted(primaryKey: true) var positionClassId: String = ""
    @Persisted var name: String = ""
    @Persisted var order: Int = 0
    @Persisted var abbreviation: String = ""
    ...
}

I want to do a migration that will change preferredPositions from a MutableSet<PositionClass> to a List<PositionClass> since now I want preferredPositions to be ordered.

So the new Player looks like:

@objc final class Player: Object {
    @Persisted(primaryKey: true) var playerId: ObjectId
    @Persisted var name: String = ""
    @Persisted var preferredPositions: List<PositionClass> = List<PositionClass>()
    ...
}

However, I can't figure out the magic incantation in the migration configuration to get access to the preferredPositions data.

In my migration I have:

    let schemaVersion: UInt64 = 22
    let config = Realm.Configuration(schemaVersion: schemaVersion,
                                    migrationBlock: { migration, oldSchemaVersion in
        ...
        if (oldSchemaVersion < 22) {
            migration.enumerateObjects(ofType: Player.className()) { oldObject, newObject in
                if let preferredPositionsSet: MutableSet<PositionClass> = oldObject!["preferredPositions"] as? MutableSet<PositionClass> {
                    let preferredPositionsList: List<PositionClass> = List()
                    preferredPositionsSet.forEach { (positionClass: PositionClass) in
                        preferredPositionsList.append(positionClass)
                    }
                    newObject!["preferredPositions"] = preferredPositionsList
                } else {
                    NSLog("preferredPositionsSet is nil.")
                }
            }
        }
    })
    Realm.Configuration.defaultConfiguration = config

But the line

let preferredPositionsSet: MutableSet<PositionClass> = oldObject!["preferredPositions"] as? MutableSet<PositionClass>

always returns nil. I've looked in the debugger and it seems like oldObject!["preferredPositions"] is a MutableSet<PositionClass>. For example if I add the code:

    let preferredPositionsAny = oldObject!["preferredPositions"]

and then look at preferredPositionsAny in the debugger it shows:

debugger showing type

So, the underlying type is correct, but I don't know how to get at it properly.

Or am I supposed to do the migration in a different way?


Solution

  • Code TL;DR:

        let schemaVersion: UInt64 = 22
        let config = Realm.Configuration(schemaVersion: schemaVersion,
                                        migrationBlock: { migration, oldSchemaVersion in
            ...
            if (oldSchemaVersion < 22) {
                migration.enumerateObjects(ofType: Player.className()) { oldObject, newObject in
                    let newRealm = newObject!.realm
                    let preferredPositionsList: List<PositionClass> = List()
                    let preferredPositionsSet = oldObject!.dynamicMutableSet("preferredPositions")
                    preferredPositionsSet.forEach { positionClass in
                        if let newPositionClass = newRealm?.object(ofType: PositionClass.self, forPrimaryKey: positionClass["positionClassId"]) {
                            preferredPositionsList.append(newPositionClass)
                        }
                    }
                    newObject!["preferredPositions"] = preferredPositionsList
                }
            }
    

    More Complete Answer:

    There were a couple of things I was getting wrong:

    1. the data type for the "old" object isn't a MutableSet<PositionClass>, but a MutableSet<DynamicObject>. This was why the cast to MutableSet<PositionClass> was always returning nil.
    2. There's also a clearer way to fetch the old MutableSet, so instead of calling
    let preferredPositionsSet = oldObject!["preferredPositions"] as! MutableSet<DynamicObject>
    

    from https://stackoverflow.com/a/37588630/1204250 I found

    let preferredPositionsSet = oldObject!.dynamicMutableSet("preferredPositions")
    

    Which seems to have the return the same object, but again, a bit more clear

    1. From this answer: https://stackoverflow.com/a/37588630/1204250, to re-populate the new List with the existing PositionClass instances (instead of creating new ones), you need to fetch the data from the Realm. However in order to get a parameter to fetch the data. Since the "old" MutableSet contains DynamicObject, you need to access it using the DynamicObject method of named fields e.g. positionClass["positionClassId"] instead of accessing a property on the actual base object type e.g. positionClass.positionClassId. So that's where
    let newRealm = newObject!.realm
    if let newPositionClass = newRealm?.object(ofType: PositionClass.self, forPrimaryKey: positionClass["positionClassId"]) {
    ...
    

    comes from.