Append generic element to Realm List - swift

I'm trying to set data in a Realm database in swift via Object schema properties. Add new objects has been straightforward (and also relate and object to another), but I can't fin the way to append objects to a List property.
Let's see the snippet of code I use to create objects and add to Realm database:
func save(elements: [String : Any], realmClassName: String, realm: Realm, listObject: Object) {
// Ge properties for Realm Object subclass with name realmClassName
let properties = realm.schema[realmClassName]
// Iterate through elements array
for element in elements {
// Instantiate Realm Object through its name (realmClassName)
let thisClass: AnyClass = NSClassFromString("\(namespace).\(name)")!
let realmClass = thisClass as! Object.Type
let object = realmClass.init()
// Iterate though Object Schema properties
for property in properties?.properties as [Property]! {
object[property.name] = element[property.name]
}
// Add Object to Realm database
realm.add(object)
// Here is where I want to append object to Object List
}
}
Question is, how to do something similar to:
listObject.append(object)
Some attempt trow error like:
Could not cast value of type 'RealmSwift.List<AppName.Cars>' (0x1117446d8) to 'RealmSwift.List<RealmSwift.Object>'
Thanks.

Related

Query MongoDB Realm array only at first index

I need to query MongoDB Realm from synced iOS and Android app. In Swift I can write something like this:
let dictionary = realm?.objects(myReamlObject.self)
let results = dictionary?.where {
$0.senses.glosses.term == "the term I want"
}
or using predicate:
let results = dictionary?.filter("ANY senses.glosses.term == %#", "the term I want")
Both work well, but I don't want to check ALL senses.glosses.term.
Every entry has (or could have) many senses and many glosses.
I would like to check term of first senses in first glosses only.
Something I would write like this:
let results = dictionary?.where {
$0.senses[0].glosses[0].term == "the term I want"
}
But it gives error:
Referencing subscript 'subscript(_:)' on 'Query' requires that
'List<myRealmObject_senses>' conform to 'RealmKeyedCollection'
Any suggestion on how to query only first index of an array in MongoDB Realm? Thank you
Let me re-state the question
How to query a Realm objects' List property - but only query on the first
element in the List.
The answer is going to depend on the amount of results being worked with.
Here's how to do it in a way that's O.K. for small datasets but NOT RECOMMENDED
Your models were not included in the question so let me use a simplified model of a PersonClass that as a List of DogClass objects
class PersonClass: Object {
#Persisted var name = ""
#Persisted var dogList = List<DogClass>()
}
class DogClass: Object {
#Persisted var name = ""
}
The idea here is to use Swift high-level functions to only test the first item in each persons doglist for a match (this can be applied to other languages as well)
//get all the people
let peopleResults = realm.objects(PersonClass.self)
//use the high-level Swift function compactMap to return all of
// the people whose first dog is named "Spot"
let persons = peopleResults.compactMap { person -> PersonClass? in
if person.dogList.first?.name == "Spot" {
return person
}
return nil
}
The downside is that this code overrides a fundamental advantage of Realm - that Realm objects are lazily loaded if Realm functions are used e.g. as soon as a high-level Swift function is used, ALL of the objects are loaded into memory, potentially overwhelming the device.
A better option is to simply add a managed property to the PersonClass that also points to the 0'th element in the list.
class PersonClass: Object {
#Persisted var name = ""
#Persisted var dogList = List<DogClass>()
#Persisted var mainDog: DogClass?
func addMainDog(withDog: DogClass) {
self.dogList.append(withDog)
self.mainDog = withDog
}
}
as you can see, there's also a function to add that first dog to the list and also populates the mainDog property which points to the same object. It's one property so the overall impact is very low but the advantages for simple queries are very high.
From there the query becomes trivial
let peopleResults = realm.objects(PersonClass.self).where { $0.mainDog.name == "Spot" }
Expanding on this, you could save the 0th element of each List object in the Parent object or even have a property in each child object that points to the first element in it's respective list.

Using NSKeyedUnarchiver to decode object to a new model class in Swift using Coddle

I am working with an old Objective-C codebase that utilizes NSCoding, NSSecureCoding, and NSKeyedUnarchiver/NSKeyedArchiver to store a model in User Defaults. We are migrating to a new User Defaults layer and I am wondering if it possible to decode this object without having the underlying class. For instance, the current object being stored is UserModel. Is it possible for me to create a new class, NewUserModel, with the same properties then decode this object from User Defaults?
I have tried the following, see comments for results:
guard let userData: Data = UserDefaults.default.object(forKey: "user-data") else {
return nil
}
// This returns the object, but it is Any as we do not have the model class for this object
guard let restoredObject = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(userData) as? Any else {
return nil
}
let unarchiver: NSKeyedUnarchiver = NSKeyedUnarchiver(forReadingFrom: userData)
if let decoded = unarchiver.decodeTopLevelDecodable(NewUserModel.self, forKey: NSKeyedArchiveRootObjectKey) {
// this fails and does not decode the object even though the properties are identical
print(decoded)
}
// trying to access the properties individually also fails
if let userToken: String = unarchiver.decodeObject(forKey: "userToken") {
// fails
print(userToken)
}
// attempting to decode using JSONDecoder also fails as the data is not valid JSON
do {
let jsonDecoder = JSONDecoder()
let user = try jsonDecoder.decode(NewUserModel.self, from: userData)
print(user)
} catch {
print(erro)
}
Basically I have the underlying data yet I need to decode this data manually to get the relevant information without having access to the exact class that was used to archive the data.
Is it possible for me to create a new class, NewUserModel, with the same properties then decode this object from User Defaults?
Yes.
See the -setClass:forClassName: method, which is described thus:
Sets a translation mapping on this unarchiver to decode objects encoded with a given class name as instances of a given class instead.
So you can use that method to tell your unarchiver to instantiate NewUserModel whenever it sees the class name UserModel in the archive.
There's also a class method +setClass:forClassName: that does the same thing for all unarchivers, which might be handy if you have to read the same data in multiple places.

Convert filter expression to NSPredicate format

I would like to perform this filter method using NSPredicate, but I'm having trouble converting the syntax.
objectsCollection.filter { $0.stringIds.contains(id) }
Sample class
class Object {
let stringIds: [String]
}
let objectsCollection: [Object]
let id = "The id we want to look for"
objectsCollection.filter { $0.stringIds.contains(id) }
My attempt with NSPredicate
This is how I thought it would work, but seems like it doesn't.
let filter = NSPredicate(format: "%# IN stringIds", id)
objectsCollection.filter(filter)
Error
reason: 'Expected object of type (null) for property 'stringIds' on object of type 'Object', but received: 6011ea4dda6853a3af97376e'
There are a few issues that need to be addressed.
This
let stringIds: [String]
is not a realm object and will not be persisted in Realm.
That needs to be a List property and the object type of the list is another Realm object. Realm Lists do not support primitives (very well). Also, don't name your objects the same name as another object.
class MyClass: Object {
let myStringIdList = List<MyStringIdClass>
}
and
class MyStringIdClass: Object {
#objc dynamic var myId = ""
}
to then get all of the MyClass objects that had a certain stringId in their list
let results = realm.objects(MyClass.self).filter("ANY myStringIdList.myId == %#", idToFind)
The string inside .filter can also be an NSPredicate if needed.
You can also use LinkingObjects to navigate back to the MyClass objects as well.
One other thing, when you cast realm objects to an array, they loose their connection to realm and are no longer live updating objects.
Also, Realm objects are lazily loaded meaning that thousands of objects have very little memory impact. Casting them to an array however, loads ALL of that data and can overwhelm the device with a large data set.

Realm updates are not saving

So I'm trying to update a List property of an Object class in Realm. The property of the Object updates, but for some reason when it gets to the Realm, the update never occurs. In other words, I've confirmed that the changes have been made to the actual object just after I made the changes(the List updates in the object), but after I add to the realm, and then get the object back from the realm, it's as if nothing ever changed. Here is the code:
do{
try! realm.write{
let course = realm.objects(Course.self).filter("id =='\(courseID!)'").first
course!.days = List<String>()
for day in daysSelected{
course?.days.append(day)
}
realm.add(course!, update: .modified)
}
}catch{
print(error)
}
Also, you should know that when I update other properties like Strings, the changes go through just fine. Am I doing something wrong with lists? In my object class, the list is declared as:
var days: List<String> = List<String>()
According to the documentation:
Properties of List type defined on Object subclasses must be declared
as let and cannot be dynamic.
Rather than defining your list as:
var days: List<String> = List<String>()
Define it as:
let days = List<String>()

Create a dictionary out of an array in Swift

I want to create a dictionary out of an array and assign a new custom object to each of them. I'll do stuff with the objects later. How can I do this?
var cals = [1,2,3]
// I want to create out of this the following dictionary
// [1:ReminderList() object, 2:ReminderList() object, 3:ReminderList() object]
let calendarsHashedToReminders = cals.map { ($0, ReminderList()) } // Creating a tuple works!
let calendarsHashedToReminders = cals.map { $0: ReminderList() } // ERROR: "Consecutive statements on a line must be separated by ';'"
map() returns an Array so you'll either have to use reduce() or create the dictionary like this:
var calendars: [Int: ReminderList] = [:]
cals.forEach { calendars[$0] = ReminderList() }
You can also use reduce() to get a oneliner but I'm not a fan of using reduce() to create an Array or a Dictionary.