Search code examples
iosswiftinheritancerealmone-to-many

Inheritance with Swift Realm, confusion


I have an issue about Inheritance with my Objects in Realm.

Could you please have a look a it. I have :

  • an Object Activity
  • an Object Sport which I want to be a subclass of Activity
  • an Object Seminar which I want to be a subclass of Activity

To make this happen I write, according to the documentation, the following code :

// Base Model
class Activity: Object {
      dynamic var id = ""
      dynamic var date = NSDate()   

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

// Models composed with Activity
class Nutrition: Object {
    dynamic var activity: Activity? = nil
    dynamic var quantity = 0
}

  class Sport: Object {
    dynamic var activity: Activity? = nil
    dynamic var quantity = 0
    dynamic var duration = 0
}

Now I have an Model Category which I want it to hold the activities, doesn’t matter if it’s an Nutrition or Sport.

Here is my code :

class Categorie: Object {

    let activities = List<Activitie>()
    dynamic var categoryType: String = ""

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

}

Now I try to add a Nutrition object to my List<Activitie> by doing this :

let nutrition =  Nutrition(value: [ "activity": [ "date": NSDate(), "id": "0" ], "quantity": 12 ])

try! realm.write {
     realm.add(nutrition, update: true)
}

It doesn’t work because List<Activitie> expect an Activity Object and not a Nutrition Object. Where am I wrong ?

Thanks a lot for the help.


Solution

  • According to the article about type erased wrappers in swift and the #5 option I have ended up with something more flexible, here is my solution.

    ( please note that the solution #5 need to be updated for Swift 3, my solution is updated for Swift 3 )

    My main Object Activity

    class Activity: Object {
        dynamic var id = ""
    
        override static func primaryKey() -> String? {
            return "id"
        }
    }
    

    and my inheritance : Nutrition and Sport

    class Nutrition: Activity { }

    class Sport: Activity { }

    The solution according to the solution #5 option : Using a type-erased wrapper for polymorphic relationships.

    If you want to store an instance of any subclass of Activity, define a type-erased wrapper that stores the type's name and the primary key.

    class AnyActivity: Object {
        dynamic var typeName: String = ""
        dynamic var primaryKey: String = ""
    
        // A list of all subclasses that this wrapper can store
        static let supportedClasses: [Activity.Type] = [
            Nutrition.self,
            Sport.self
        ]
    
        // Construct the type-erased activity from any supported subclass
        convenience init(_ activity: Activity) {
            self.init()
            typeName = String(describing: type(of: activity))
            guard let primaryKeyName = type(of: activity).primaryKey() else {
                fatalError("`\(typeName)` does not define a primary key")
            }
            guard let primaryKeyValue = activity.value(forKey: primaryKeyName) as? String else {
                fatalError("`\(typeName)`'s primary key `\(primaryKeyName)` is not a `String`")
            }
            primaryKey = primaryKeyValue
        }
    
        // Dictionary to lookup subclass type from its name
        static let methodLookup: [String : Activitie.Type] = {
            var dict: [String : Activity.Type] = [:]
            for method in supportedClasses {
                dict[String(describing: method)] = method
            }
            return dict
        }()
    
        // Use to access the *actual* Activitie value, using `as` to upcast
        var value: Activitie {
            guard let type = AnyActivity.methodLookup[typeName] else {
                fatalError("Unknown activity `\(typeName)`")
            }
            guard let value = try! Realm().object(ofType: type, forPrimaryKey: primaryKey) else {
                fatalError("`\(typeName)` with primary key `\(primaryKey)` does not exist")
            }
            return value
        }
    }
    

    Now, we can create a type that stores an AnyActivity!

    class Category: Object {
    
        var categoryType: String = ""
        let activities = List<AnyActivity>()
    
        override static func primaryKey() -> String? {
            return "categoryType"
        }
    }
    

    and to store the data :

    let nutrition = Nutrition(value : [ "id" : "a_primary_value"] )
    
    let category = Category(value: ["categoryType" : "0"])
    
    category.activities.append(AnyActivity(tree))
    

    To read the data we want to check the activity method, use the value property on AnyActivity

    for activity in activities {
        if let nutrition = activity.value as? Nutrition {
           // cool it's a nutrition
        } else if let sport = activity.value as? Sport {
           // cool it's a Sport   
        } else {
            fatalError("Unknown payment method")
        }
    }