Realm iOS - How to Update List?

Viewed 1947

I am using Codable with Realm on iOS and is finding issue with updating existing records saved in Realm. Data consists of a list of Items where each of the items has List of categories and each category has a description.

This is how my Realm Codable models look like

class Items: Object, Decodable {

    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    let categories =  List<Categories>()
    @objc dynamic var desc: Desc?

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

    var hasSubCategories: Bool {
        if self.categories.count > 0 {
            return true
        }
        return false
    }

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case desc
        case categories
    }

    required init() {
        super.init()
    }

    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        desc = try (container.decodeIfPresent(Desc, forKey: .desc))
        let categoriesList = try container.decodeIfPresent(List<Categories>.self, forKey: .categories) ?? List<Categories>()
        categories.append(objectsIn: categoriesList)
        super.init()
    }
}

class Categories: Object, Decodable {

    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    @objc dynamic var desc: Desc?
    let categories =  List<Categories>()

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

    var hasSubCategories: Bool {
        if categories.count > 0 {
            return true
        }
        return false
    }

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case categories
   }

    required init() {
        super.init()
    }

    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        desc = try (container.decodeIfPresent(Desc, forKey: . desc))
        let categoriesList = try container.decodeIfPresent(List<Categories>.self, forKey: .categories) ?? List<Categories>()
        categories.append(objectsIn: categoriesList)
        super.init()
    }
}

I add new Items received from API with following code

let realm = try? Realm()

 for item in ItemsFromAPI {

do {
     try realm?.write {
         realm?.add(item)
       }
    }
}

When a data already saved in DB is received from API, I need to update it. As per my understanding, with primarykey implemented

realm?.add(Item, update: .modified)

will update the existing record which has the same primary. Properties of object with class Item are updated but the List<> of categories are not getting updated.

I tried to fetch existing list of categories saved in database and mutated the categories object associated with savedItem by calling savedItem.categories.removeAll() and added new categories with savedItem.categories.append(objectsIn: itemFromAPI.categories) When i save this with

realm?.add(Item, update: .modified)

Realm crashing throwing an exception - “RLMException reason: Attempting to create an object of type with an existing primary key value”

So I tried removing the category with

realm?.delete(Category)

and then added the modified Category associated with Item and this is working.

I have nested Categories to multiple levels and updating them by fetching it again, deleting them and readding them again is proving to be a pain. Is this how update records work in realm?

Can I update the nested category related to an item by simply assigning the new object against an item and use the

realm?.add(Item, update: .modified) 

to update the records like its done for adding a new item?

Any response would be greatly appreciated.

1 Answers

Per a comment, the question is

My question is how to update a Realm List property with realm?.add(Item, update: .modified) function.

Let me restate the question for clarity

How to update an object stored in another objects List property where the stored objects in the list have a primary key

The short answer is: a List object holds references to other objects within that list. Because it contains references, the List itself doesn't need to be updated, just the object it's referencing

The long answer is via an example: Let's take a Person who has a List property of Dogs

class PersonClass: Object {
   @objc dynamic var person_id = UUID().uuidString
   let dogs = List<DogClass>()

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

class DogClass: Object {
   @objc dynamic var dog_id = NSUUID().uuidString
   @objc dynamic var dog_name = ""

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

The key is that a List object contains references to the objects stored within it. It doesn't 'hold' those actual objects. If the properties of an object in a list change, the list itself is unaffected and still has a reference to that changed object.

So say our person has two dogs.

let person = PersonClass()

let dog0 = DogClass()
dog0.dog_name = "Spot"
let dog1 = DogClass()
dog1.dog_name = "Rover"

person.dogs.append(objectsIn: [dog0, dog1])

try! realm.write {
    realm.add(person)
}

so now person has two dogs; Spot and Rover.

Suppose we want to change Rovers name to Lassie. There's no need to change or update the persons list - simply update the dogs name property in realm, ensuring we use the same dog_id, and that change will be reflected in the person list.

let updatedDog = DogClass()
updatedDog.dog_id = "the dogs primary key"
updatedDog.dog_name = "Lassie"

try! realm.write {
   realm.add(updatedDog, update: .modified)
}
Related