RealmCollectionChange modified parameter has multiple indexes when only one object is modified - swift

Issue: When a single object is modified in Realm, the modified parameter in a RealmCollectionChange is passed multiple indexes
Details: Given a Person object which has a list of Dog Objects and a Dog object that has an owner property of type Person
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var owner: Person?
}
class Person: Object {
#objc dynamic var name = ""
let dogs = List<Dog>()
}
then a single person is created with two dogs
let person0 = Person()
person0.personName = "Leroy"
let dog0 = Dog()
dog0.name = "Spot"
dog0.owner = person0
let dog1 = Dog()
dog1.name = "Rex"
dog1.owner = person0
person0.dogs.append(dog0)
person0.dogs.append(dog1)
//write person0 to Realm which creates the person and two dogs
and the observe function
func doObserve() {
let realm = try! Realm()
let results = realm.objects(Dog.self)
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
switch changes {
case .initial:
case .update(_, let deletions, let insertions, let modifications):
for index in deletions {
print("deleted object at index: \(index)")
}
for index in insertions {
print("inserted object at index: \(index)")
}
for index in modifications {
print("modified object at index: \(index)")
let object = results[index]
print(object) //prints dog0 and dog1 when dog1 is mod
}
case .error(let error):
fatalError("\(error)")
}
}
}
then, if the Realm file is opened with RealmBrowser (or using code in the App) and Dog1 (Rex) name is changed to Fluffy, the modified parameter within the RealmCollectionChange contains two indexes, 0 and 1 even though only dog1 (index 1) was modified.
Is this behavior correct and if so, is there another option to just get the (index to the) object that was modified?
(or am I totally missing something obvious?)

When you modify dog1, dog0.owner.dogs[1] has changed, so Realm reports that dog0 is modified as well.
There's currently no way to manually specify how deep of a path you want Realm to check for changes to an object, but you may be able to sidestep this behavior by changing Person's List<Dog> to LinkingObjects<Dog>, as inverse relationships are not followed when checking for changes. This also would remove the need to set the relationship in two places.

Related

Add Data in multiple classes Realm swift

I'm new to swift. I'm working with realm in swift. I've a realm database which have following classes.
I'm inserting same data in each class from application as:
for item in myarray {
print(item)
let realm = try! Realm(fileURL: db_path!)
try! realm.write({
let obj = English()
obj.rowid = ((item["rowid"] as? NSString)?.integerValue)!
obj.words = (item["word"] as? NSString)! as String
obj.frequency = ((item["frequency"] as? NSString)?.integerValue)!
realm.add(obj)
print("successfully added to database")
})
}
class English : Object {
#objc dynamic var rowid = 0
#objc dynamic var words = ""
#objc dynamic var frequency = 0
}
This is saving data in English class. Now I want to save same object in other realm class like Arabic, languages, test.
Should I've to make separate object type class for each class?
No ,you can create object of each class and assign English class object to other like this
let obj1 = Arabic()
obj1 = obj//English Class Object
And append like this
realm.add(obj1)

Why can't I get the index of a filtered realm List?

I'm trying to find the index of an item in a List<> object after the item has been appended to it, so that I can insert into a tableview.
The tableview is sectioned with .filters so I have to apply the filter before looking for the indexPath. However, the filter appears to break the indexOf functionality.
I noticed that the function .map has the same effect.
import UIKit
import RealmSwift
class Model: Object {
#objc dynamic var title: String = ""
let items = List<Model>()
}
class ViewController: UIViewController {
var models: Results<Model>?
var parentModel: Model?
var items = List<Model>()
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
if !UserDefaults.standard.bool(forKey: "IsNotFirstTime") {
populateRealm()
UserDefaults.standard.set(true, forKey: "IsNotFirstTime")
}
models = realm.objects(Model.self)
parentModel = models!.first
items = parentModel!.items
let child = Model()
child.title = "Child"
try! realm.write {
parentModel!.items.append(child)
}
print(items.index(of: child)) // prints correct value
print(items.filter({ $0.title == "Child" }).index(of: child)) // prints nil
}
func populateRealm() {
let parent = Model()
parent.title = "Parent"
try! realm.write {
realm.add(parent)
}
}
}
The first print finds the object, but the second print doesn't, despite the mapping having no overall effect.
The strange thing is that the object IS in the filtered list, doing:
print(items.filter({ $0.title == "Child" }).first
Returns the object, so it is there.
Edit
On further inspection, it looks like it's not the filter but the conversion of array type that breaks the functionality, converting to array without a filter does the same thing.
print(Array(items).index(of: child)) // prints nil
When you want to use mapping you should add an attributes to map your objects according to it for example
print(items.map({ $0.id }).index(of: child.id))
if you use it on this way it will return what you expected
I figured out the solution. The filter syntax I used .filter({ $0.title == "Child" }) isn't the Realm filter, and converts the List to a LazyFilterCollection<List<Model>>, which doesn't seem to be compatible with searching for the index of a realm object.
The fix was to use the format .filter("title == %#", "Child"), which returns a realm Results object.

Realm + swift sorting

Wondering if anyone have faced & solved this issue:
Description
I have 2 Classes called Person & Dog where a person can have more than one dog.
class Dog: Object {
dynamic var name = ""
dynamic var age = 0
}
class Person: Object {
dynamic var name = ""
dynamic var picture: NSData? = nil // optionals supported
let dogs = List<Dog>()
}
According to my requirement, I need to fetch all objects from person.name == "ABC" & apply sort all dogs by Name.
Snippet:
let realm = try! Realm()
let result = realm.objects(Person.self).filter("name == %#","ABC").sorted(property : "name")
From the above snippet, sorting is done based on the person Name not on the dog's Name.
How to create a filter & sort the list by dog name?
realm.objects(Person.self) returns Results<Person>. Calling sorted() to Results<Person> means sort Person. If you'd like to sort dogs, you can retrieve element of person first, (e.g. let person = result[0]), then retrieve dogs property, call sorted() method to it.
let people = realm.objects(Person.self).filter("name == %#","ABC")
let person = people.first!
let sortedDogs = person.dogs.sorted(property : "name")

realm. remove objects from ListBase

I have different realm models. They have List properties. I want to make universal way for removing objects from List properties. So I did the following:
if let list = self[property.name] as? ListBase {
list._rlmArray.removeAllObjects()
}
but this just clear list property, without deleting objects from realm. The only way I've found is:
if let list = self[property.name] as? ListBase {
while list.count > 0 {
let object = list._rlmArray.firstObject()
let any = object as Any
if let theObject = any as? Object {
realm.delete(theObject)
}
}
}
Code above works and doesn't generate any warning. But it looks ugly.
You can use dynamicList(_ propertyName: String) to retrieve List property by name instead subscript.
if property.type == .array {
try! realm?.write {
realm?.delete(dynamicList(property.name))
}
}

Deleting with one-to-many relationship

I have a one-to-many relationship:
class GameSystem: Object {
dynamic var gameSystemName = ""
}
class games: Object {
dynamic var gameSystemName = gameSystemName().name
dynamic var gameTitle = ""
dynamic var gameGenre = ""
}
The gameSystemNames are currently displayed on a TableView. If the user deletes a gameSystemName, I want that gameSystemName along with all of that system's games deleted.
The code I'm currently using will only delete the GameSystem, but leaves all the games.
func deleteRowAtIndexPath(indexPath: NSIndexPath) {
let realm = Realm()
let objectToDelete = gameSystems[indexPath.row]
realm.write {
realm.delete(objectToDelete)
}
gameSystemTableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
gameSystems = Realm(path: Realm.defaultPath).objects(GameSystem)
}
I'm assuming there's a simple way to do this.
If you keep your model as it is, the solution would be to query first for the relevant objects of the relation Game:
// …
let objectToDelete = gameSystems[indexPath.row]
let gameSystemName = objectToDelete.gameSystemName
realm.write {
let games = realm.objects(Game).filter("gameSystemName = ?", gameSystemName)
realm.delete(games)
realm.delete(objectToDelete)
}
// …
Model Recommendation
I'd propose instead that you add an explicit link to your model, instead of expressing the relationship through a loosely linked foreign key. But the object-mapping is very individual and may be dependent on further constraints going beyond the scope of your question and this answer. For further reference, that would look like below:
class GameSystem : Object {
dynamic var name = ""
let games = List<Game>()
}
class Game : Object {
dynamic var title = ""
dynamic var genre = ""
// Use a backlink
// (https://realm.io/docs/swift/latest/#inverse-relationships)
dynamic var gameSystem: GameSystem? {
return linkingObjects(GameSystem.self, forProperty: "games").first
}
}
If you setup your model like this, you can delete your games very easy:
// …
let objectToDelete = gameSystems[indexPath.row]
realm.write {
realm.delete(objectToDelete.games)
realm.delete(objectToDelete)
}
// …
Note: In the future, Realm will bring Cascading Deletes as feature. Once that is released, you won't even need to take care of the manual deletion of associated games, but you will rather be able to declare the strong relationship in your model, so that the Games are automatically deleted.
Alternative Link Declaration
You can also declare your link vice-versa, but that would make it likely harder to use a feature like Cascading Deletes in the future. However the code to delete them for now would look the same as above.
class GameSystem : Object {
dynamic var name = ""
// Use a backlink
// (https://realm.io/docs/swift/latest/#inverse-relationships)
dynamic var games: [Game] {
return linkingObjects(Game.self, forProperty: "gameSystem")
}
}
class Game : Object {
dynamic var title = ""
dynamic var genre = ""
let gameSystem: GameSystem? = nil
}
It is very easy to delete the Parent as well as childrens in REALM SWIFT... You just need to write a small piece of code.
Here I am trying to delete my main category called "Swift" and whenever my main category gets deleted all the Sub Categories are also deleted...
do{
try realm.write({
realm.delete(Category.items)
realm.delete(Category)
})
}catch{
print("ERROR WHILE DELETING CELL ::: \(error)")
}
In short to delete subcategories all you need is to place "items" after . followed by parent name.