Search code examples
iosrealm

Why does adding a convenience init to a Realm object declaration mess with private values?


I have created a Realm object that needs to store an enum value. To do that I use a method outlined in this question which involves declaring a private property of type String, and then declaring another property of type Enum that sets/reads the private property using getters and setters.

For ease of reference here is the code for that:

@objcMembers
class PlaylistRealmObject: Object {

    dynamic var id: String = UUID().uuidString
    dynamic var created: Date = Date()
    dynamic var title: String = ""
    private dynamic var revisionTypeRaw: String = RevisionType.noReminder.rawValue
    var revisionType: RevisionType {
        get { return RevisionType(rawValue: revisionTypeRaw)! }
        set { revisionTypeRaw = newValue.rawValue }
    }
    let reminders = List<ReminderRealmObject>()
    let cardsInPlaylist = List<CardRealmObject>()

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

}

I have noticed though that if I add a convenience init to the class declaration (to make it a bit easier to initialise the object) the revisionType properties on the objects I end up with adopt the default value declared in the class, and NOT the revision type value passed to the class using the convenience init.

Here is the class declaration with a convenience init

@objcMembers
class PlaylistRealmObject: Object {

    dynamic var id: String = UUID().uuidString
    dynamic var created: Date = Date()
    dynamic var title: String = ""
    private dynamic var revisionTypeRaw: String = RevisionType.noReminder.rawValue
    var revisionType: RevisionType {
        get { return RevisionType(rawValue: revisionTypeRaw)! }
        set { revisionTypeRaw = newValue.rawValue }
    }
    let reminders = List<ReminderRealmObject>()
    let cardsInPlaylist = List<CardRealmObject>()

    convenience init(title: String, revisionType: RevisionType) {
        self.init()
        self.title = title
        self.revisionType = revisionType
    }

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

}

And - to make things even more perplexing - if I simply remove the word 'private' from the revisionTypeRaw property, everything works fine!

I am confused. 1) Why does adding a convenience init have this effect? 2) Why does making the property 'public' resolve the issue?

I have created a demo Xcode project to illustrate the issue and can share it if anyone needs it.


Update: I found the problem. It has nothing to do with the convenience init. I am using @objcMembers at the top of the class as per the Realm docs: https://realm.io/docs/swift/latest/#property-attributes

If you remove this and place @objc in front of the private keyword, everything works as would be expected. I guess the question then is: what explains this behaviour?


Solution

  • This is a good question but I think the issue is elsewhere in the code. Let's test it.

    I created a TestClass that has a Realm managed publicly visible var, name, as well as a non-managed public var visibleVar which is backed by a Realm managed private var, privateVar. I also included a convenience init per the question. The important part is the privateVar is being set to the string "placeholder" so we need to see if that is overwritten.

    class TestClass: Object {
        @objc dynamic var name = ""
        @objc private dynamic var privateVar = "placeholder"
    
        var visibleVar: String {
            get {
                return self.privateVar
            }
            set {
                self.privateVar = newValue
            }
        }
    
        convenience init(aName: String, aString: String) {
            self.init()
            self.name = aName
            self.visibleVar = aString
        }
    }
    

    We then create two instances and save them in Realm

    let a = TestClass(aName: "some name", aString: "some string")
    let b = TestClass(aName: "another name", aString: "another string")
    
    realm.add(a)
    realm.add(b)
    

    Then a button action to load the two objects from Realm and print them.

    let testResults = realm.objects(TestClass.self)
    
    for test in testResults {
        print(test.name, test.visibleVar)
    }
    

    and the output:

    some name some string
    another name another string
    

    So in this case, the default value of "placeholder" is being overwritten correctly when the instances are being created.

    Edit:

    A bit more info.

    By defining your entire class with @objMembers, it exposes your propertites to Objective-C, but then private hides them again. So that property is not exposed to ObjC. To reverse that hiding, you have to say @objc explicitly. So, better practice is to define the managed Realm properties per line with @objc dynamic.