Realm iOS - How to Update List? - ios

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.

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

Subclass currently has no primary key relationship

I have JSON that looks like this, which returns a list of Posts:
[
{
"id" : 1,
"message": "Hello"
"urls" : {
"png" : "https://example.com/image.png",
"jpg" : "https://example.com/image.jpg",
"gif" : "https://example.com/image.gif"
}
}
]
As you can see, I need to make two classes. One for the parent object (Post), and one for the object "urls" (PostUrls).
I've done that like so:
class Post: Object, Decodable {
#objc dynamic var id = 0
#objc dynamic var message: String? = nil
#objc dynamic var urls: PostUrls? = nil
override static func primaryKey() -> String? {
return "id"
}
private enum PostCodingKeys: String, CodingKey {
case id
case message
case urls
}
convenience init(id: Int, message: String, urls: PostUrls) {
self.init()
self.id = id
self.message = message
self.urls = urls
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostCodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let message = try container.decode(String.self, forKey: .message)
let urls = try container.decode(PostUrls.self, forKey: .urls)
self.init(id: id, message: message, urls: urls)
}
required init() {
super.init()
}
}
And
#objcMembers class PostUrls: Object, Decodable {
dynamic var png: String? = nil
dynamic var jpg: String? = nil
dynamic var gif: String? = nil
private enum PostUrlsCodingKeys: String, CodingKey {
case png
case jpg
case gif
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostUrlsCodingKeys.self)
png = try container.decodeIfPresent(String.self, forKey: .png)
jpg = try container.decodeIfPresent(String.self, forKey: .jpg)
gif = try container.decodeIfPresent(String.self, forKey: .gif)
super.init()
}
required init() {
super.init()
}
}
But, the problem is that I have no relationship between Post and PostUrls, since there is no primary key to connect the two. Further, this also means that I currently won't be able to control duplicates inside the PostUrls table.
So my question is: how can I create a relationship between the two tables, and prevent duplicates in the PostUrls table?
In this case, you do have a relationship between those objects. Object Post contains object PostUrls. Realm does not require a primary key to have this kind of relationship, because it creates a primary key behind the scenes. So it uses it, even though you can't access it.
To manually set a primaryKey you have to override a func called primaryKey()
#objcMembers class DBFilterModel: Object {
// MARK: Properties
dynamic var id: Int = 0
override public static func primaryKey() -> String? {
return "id"
}
}
This way you tell to realm that you want this property to be used as a Unique Key.
To prevent duplicating them, there are 2 ways. First - try to find an object with that id already existing in your database, if it exists - don't create it.
Second - by adding conflicts handlers to Realm's save methods. You can set that objects with same ID's will be just modified, but not duplicated. Or you could say that you want to throw an error when you try to insert a duplicated object.
realm.add(objects, update: update ? .modified : .error)
The question has two questions within
How do you create a relationship between the two 'tables'
prevent duplicates
Let me address 1)
Start with a class to hold the image type (.jpg etc) and then the url
class ImageUrlClass: Object {
#objc dynamic var imageType = ""
#objc dynamic var imageUrl = ""
}
and then the main class which handles decoding
class Post: Object, Decodable {
#objc dynamic var id: Int = 0
#objc dynamic var message: String = ""
let urlList = List<ImageUrlClass>()
...edited for brevity
convenience init(id: Int, message: String, urls: [String: String]) {
self.init()
self.id = id
self.message = message
//create a ImageUrlClass from each dictionary entry
for url in urls {
let key = url.key
let value = url.value
let aUrl = ImageUrlClass(value: ["imageType": key, "imageUrl": value])
self.urlList.append(aUrl)
}
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostCodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let message = try container.decode(String.self, forKey: .message)
let urls = try container.decode([String: String].self, forKey: .urls)
self.init(id: id, message: message, urls: urls)
}
}
The above will create a Post object with a List property that contains the image types and urls (a List behaves very similar to an array)
You could further this by adding a LinkingObjects property to the ImageUrlClass which would automagically create an inverse relationship to the Post object when objects are added to the List. Not sure if you need that but it's available.
You can this do this to print out the post properties
let decoder = JSONDecoder()
let post = try! decoder.decode(Post.self, from: encodedData)
print(post.id)
print(post.message)
for url in post.urlList {
let a = url.imageType
let b = url.imageUrl
print(a,b)
}
which would results in an output like this
1
Hello
jpg https://example.com/image.jpg
png https://example.com/image.png
gif https://example.com/image.gif

iOS Swift Couchbase Lite Primary Key and Class Configuration

I was going through couchbase-lite to use it in my next iOS app. I have created a model named Surah for now. Definitely, I will have more model classes later.
Basically I have four questions here.
How do I add _id as my primary key in couchbase-lite?
As I will be having more classes how will I handle those? As I am creating
MutableDocument, How will that differentiate each my classes?
As I can see I have to iterate through each of my items to batch insert, won't that become slow for the large datasets?
How do i convert results from a query with large data to a array of Model Class. (in this case of array of Surah)
class Surah: Decodable {
enum Keys: String, CodingKey {
case _id
case index
case englishName
case englishMeaning
case name
case place
case count
}
var _id = ""
var index = 1
var page = 1
var numberOfAyahs = 1
var englishName = ""
var englishMeaning = ""
var name = ""
var place = ""
var isFavorite = false
var dictionary: [String: Any] {
return ["_id": _id, "index": index, "page": page]
}
required init() {}
required init(_id: String, index: Int, name: String, englishName: String, englishMeaning: String, place: String, count: Int) {
self._id = _id
self.index = index
self.name = name
self.englishName = englishName
self.englishMeaning = englishMeaning
self.place = place
self.numberOfAyahs = count
}
required convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self) // defining our (keyed) container
let _id: String = try container.decode(String.self, forKey: ._id)
let index: Int = try container.decode(Int.self, forKey: .index)
let name: String = try container.decode(String.self, forKey: .name)
let englishName: String = try container.decode(String.self, forKey: .englishName)
let englishMeaning: String = try container.decode(String.self, forKey: .englishMeaning)
let place: String = try container.decode(String.self, forKey: .place)
let count: Int = try container.decode(Int.self, forKey: .count)
self.init(_id: _id, index: index, name: name, englishName: englishName, englishMeaning: englishMeaning, place: place, count: count)
}}
Code for Database Queries
let surahs = try JSONDecoder().decode([Surah].self, from: data!)
DispatchQueue.global(qos: .background).async {
//background code
do {
if let db = App.shared.database {
try db.inBatch {
for item in surahs {
let doc = MutableDocument(data: item.dictionary)
doc.setString("users", forKey: "type")
doc.setValue(Keys._id, forKey: item._id)
// doc.setValue(Keys.englishName, forKey: item.englishName)
try db.saveDocument(doc)
let index = IndexBuilder.valueIndex(items:
ValueIndexItem.expression(Expression.property("_id")), ValueIndexItem.expression(Expression.property("type")))
try db.createIndex(index, withName: "TypeNameIndex")
print("saved user document \(doc.string(forKey: "englishName"))")
}
}
}
} catch let error {
DispatchQueue.main.async {
seal.reject(error)
}
}
DispatchQueue.main.async {
seal.fulfill(surahs)
}
}
Not sure what you mean by "primary key". You can always find a doc
by its id. The name of the field that contains it is Meta.id.
The field's value is mutableDoc.getId(). As you've noticed, you
can also explicitly set the id at creation
Couchbase doesn't store classes, it stores JSON documents. If you
have documents of different types (different internal structures,
analogous to different SQL tables), give them a type field and use
it in your query
Use Database.inBatch()
The same way you would convert any JSON document to a corresponding
class: gson, Jackson, Moshi, etc

Updating a saved Codable struct to add a new property

My app uses a codable struct like so:
public struct UserProfile: Codable {
var name: String = ""
var completedGame: Bool = false
var levelScores: [LevelScore] = []
}
A JSONEncoder() has been used to save an encoded array of UserProfiles to user defaults.
In an upcoming update, I'd like to add a new property to this UserProfile struct. Is this possible in some way?
or would I need to create a new Codable struct that has the same properties plus one new property, and then copy all the values over to the new struct and then start using that new struct in place of anywhere that the UserProfile struct was previously used?
If I simply add a new property to the struct, then I won't be able to load the previous encoded array of UserProfiles as the struct will no longer have matching properties. When I get to this code for loading the saved users:
if let savedUsers = UserDefaults.standard.object(forKey: "SavedUsers") as? Data {
let decoder = JSONDecoder()
if let loadedUsers = try? decoder.decode([UserProfile].self, from: savedUsers) {
loadedUsers doesn't decode if the properties that UserProfile had when they were encoded and saved do not contain all the current properties of the UserProfile struct.
Any suggestions for updating the saved struct properties? Or must I go the long way around and re-create since I didn't already plan ahead for this property to be included previously?
Thanks for any help!
As mentioned in the comments you can make the new property optional and then decoding will work for old data.
var newProperty: Int?
Another option is to apply a default value during the decoding if the property is missing.
This can be done by implementing init(from:) in your struct
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
completedGame = try container.decode(Bool.self, forKey: .completedGame)
do {
newProperty = try container.decode(Int.self, forKey: .newProperty)
} catch {
newProperty = -1
}
levelScores = try container.decode([LevelScore].self, forKey: .levelScores)
}
This requires that you define a CodingKey enum
enum CodingKeys: String, CodingKey {
case name, completedGame, newProperty, levelScores
}
A third option if you don't want it to be optional when used in the code is to wrap it in a computed non-optional property, again a default value is used. Here _newProperty will be used in the stored json but newProperty is used in the code
private var _newProperty: Int?
var newProperty: Int {
get {
_newProperty ?? -1
}
set {
_newProperty = newValue
}
}

Realm List not stored in Swift 4.2 in release config

I've just built the latest version of my app, and have run into a problem where the Lists on all of my realm objects are not being stored.
Here is some sample code:
Object:
public class ReportItem: Object {
#objc dynamic var id: String!
#objc dynamic var someDate: Date?
// This contains another List as one of its properties
let list = List<OtherRealmObject>()
override public class func primaryKey() -> String {
return "id"
}
convenience public init(id: String, date: Date) {
self.init()
self.id = id
self.date = date
}
}
This object is being created by a json mapper from the response of a network request:
// Convert json to dictionary then
guard let id = json["id"] as? String else {
return nil
}
let date = json["date"] as? Date
let objects = json["someObjects"] as? [String: Any]
let someRealmObjects = [OtherRealmObject]()
objects.forEach { object in
// Create some realm object
someRealmObjects.append(newSomeRealmObject)
}
let reportItem: ReportItem?
if let date = date?.convertToDateFromString() {
reportItem = ReportItem(id: id, date: date)
} else {
return nil
}
reportItem!.list.append(objectsIn: someRealmObjects)
return reportItem!
Then this is passed back to my view controller, and stored like so:
// Report item is the item we just created in the json mapper
someNetworkOperation.success = { reportItem in
DispatchQueue.main.sync {
let realm = try! Realm()
try! realm.write {
realm.add(reportItem, update: true)
}
}
}
The item is then retrieved somewhere else, however list is empty, and when I try and filter I get the error This method may only be called on RLMArray instances retrieved from an RLMRealm. For some reason my list is not being persisted when I add the report object to the database.
This used to work, however in the last week or so it has stopped working. I'm wondering if it's to do with updating to Swift 4.2/Xcode 10. Also, my code just works fine in debug, not in release. Has anyone else run into this issue?
This was because during the Swift 4.2 conversion Reflection Metadata Level was somehow set to None instead of All. 🤦‍♂️
After Realm's latest update, the syntax has changed.
The to-many relationship should now be preceded by #Persisted. It also cannot be a let constant:
#Persisted var items = List<Item>()

Adding new Object to existing List in Realm

I have two classes. First looks like that:
class Person: Object {
dynamic var owner: String?
var dogs: List<Dogs>()
}
and second class which looks like that:
class Dogs: Object {
dynamic var name: String?
dynamic var age: String?
}
and now in ViewController in 'viewDidLoad' I create object Person with empty List and save it in Realm
func viewDidLoad(){
let person = Person()
person.name = "Tomas"
try! realm.write {
realm.add(Person.self)
}
}
it works great and I can create Person, problem begins when I try to read this data in SecondViewController in ViewDidLoad doing it:
var persons: Results<Person>?
func viewDidLoad(){
persons = try! realm.allObjects()
}
and try to add new Dog to List doing it in button action:
#IBAction func addDog(){
let newDog = Dogs()
newDog.name = "Rex"
newDog.age = "2"
persons[0].dogs.append(newDog)
// in this place my application crashed
}
Here my app is crashing with an information: Can only add, remove, or create objects in a Realm in a write transaction - call beginWriteTransaction on an RLMRealm instance first. How can I add new Dog to List and how can I update person[0]?
I use SWIFT 3.0
The persons property is of type Results<Person>, which is a collection containing Person objects that are managed by a Realm. In order to modify a property of a managed object, such as appending a new element to a list property, you need to be within a write transaction.
try! realm.write {
persons[0].dogs.append(newDog)
}
Write something like this:
if let person = persons?[0] {
person.dogs.append(newDog)
}
try! realm.write {
realm.add(person, update: true)
}
Please check how are you getting realm. Each time you call defaultRealm, you are getting new realm.
Side Note: Besides adding the code inside the write transaction which solves your issue, you could query Person by name as follow...
#IBAction func addDog(){
let newDog = Dogs()
newDog.name = "Rex"
newDog.age = "2"
let personName = realm.objects(Person.self).filter("name = 'Tomas'").first!
try! realm.write {
personName.dogs.append(newDog)
}
}
Add object for Realm Database
class Task : Object {
#objc dynamic var id : Int = 0
#objc dynamic var name = ""
#objc dynamic var phone = ""
#objc dynamic var address = ""
}
#IBAction func buttonSave(_ sender: Any) {
let realm = try! Realm()
let user = Task()
user.id = 0
user.name = (txtName.text! as NSString) as String
user.phone = (txtPhone.text! as NSString) as String
user.address = (txtAddress.text! as NSString) as String
try! realm.write {
realm.add(user)
print("user:",user.name)
}
}

Resources