Access the JSONString in mapping using Swift ObjectMapper library - swift

I would like to return the native JSON String to store in Realm, since Realm can't store collections of native objects.
Example JSON:
{ "root": { "id":1, "name":"name", "array":["a", "b", "c"] }}
func mapping(map:Map) {
id <- map["id]
name <- map["name"]
array <- map["array"].JSONString // array = "[\"a\", \"b\", \"c\"]"
}
Is this possible?
Same question on github https://github.com/Hearst-DD/ObjectMapper/issues/730

If it helps, Realm can indeed store collections of sub-objects.
class StringObject: Object {
dynamic var string = ""
}
class Root: Object {
dynamic var id = 0
dynamic var name = ""
let array = List<StringObject>()
}
Beyond that, if I understand ObjectMapper correctly, by the time map["array"] is already passed in mapping, it's already been converted from JSON to a Swift object. In which case, you'd need to reserialize it back into JSON yourself.

Related

Realm Swift Migration: String Wrapper List to String List

I am trying to migrate our Realm Swift schema from using a String wrapper class to the primitive String for collections. I am running into issues extracting the string values from the wrapper during migration.
Here is the wrapper class:
class StringDB: Object {
#objc var stringVal: String = ""
convenience init(string: String) {
self.init()
stringVal = string
}
}
Then I have an example class with a List<StringDB> property:
class MyObjDB: Object {
var emails: List<StringDB> = List<StringDB>()
#objc dynamic var id: String = UUID().uuidString
convenience init(_ emails: [StringDB]) {
self.init()
for email in emails {
self.emails.append(objectsIn: emails)
}
}
override static func primaryKey() -> String? {
return "id"
}
}
that I want to convert to a List<String>.
Here is my migration code.
let config = Realm.Configuration(schemaVersion: latestSchemaVersion, migrationBlock: {
migration, version in
if version < 2 {
migration.enumerateObjects(ofType: MyObjDB.className(), { old, new in
guard let old = old, let new = new else { return }
let oldEmails = old["emails"] as! List<StringDB>
let newEmails = List<String>()
for email in oldEmails {
newEmails.append(email.stringVal)
}
new["emails"] = newEmails
})
}
})
However, the let oldEmails = old["emails"] as! List<StringDB> cast fails. I've tried also casting the collection to List<MigrationObject> and then casting the individual objects to StringDB but that cast fails as well.
I've found a workaround that may be satisfactory (I haven't confirmed yet), of converting the MigrationObject directly to string using coercion like "\(email)" and then running a regEx that will extract a desired substring from the garbage (StringDB {\n\tstringVal = email#address.com;\n}), but I have no idea yet whether that will hold up in production, and I would prefer to work with something resembling a recommended way for doing this migration.
The Realm version is not shown in the question and there are a few typo's in the code. For older Realm versions, Lists should be defined thusly:
let emails = RealmSwift.List<StringDB>() //need RealmSwift. to differentiate it from Swift List
then newer versions should be this:
#Persisted var emails = RealmSwift.List<StringDB>()
Then this is a problem as it iterates over the emails array count times and appends the entire emails list over and over for every email in that list.
for email in emails {
self.emails.append(objectsIn: emails)
}
Also, when using older Realm versons with #Objc property types, they need to include dynamic. So on the StringDB object this
#objc var stringVal: String = ""
should be this
#objc dynamic var stringVal: String = ""
Lastly, the MyObjDB needs to have somewhere to put the new email list. You can't overwrite the old one as it's not the correct type. So add a property
class MyObjDB: Object {
var emails: List<StringDB> = List<StringDB>()
let updatedEmailList = List<String>()
Then to the question: See comments in code for the flow
How about this:
migration.enumerateObjects(ofType: MyObjDB.className()) { oldItem, newItem in
//instantiate a List of the old email objects as migration objects
let oldEmailList = oldItem!["emails"] as! List<MigrationObject>
//we're going to populate a new List with the email strings
var newEmailList = List<String>()
//iterate over the old list, extracting the string from the old object as
// a string an inject it into a new List of Strings
for oldEmailObject in oldEmailList {
let oldEmail = oldEmailObject["stringVal"] as! String
newEmailList.append(oldEmail)
}
//assign the new List of strings to a new emails property
newItem!["updatedEmailList"] = newEmailList
}

Problem with mapping an array of objects in Swift

I have the following 2 class / structs:
class ConversationDetails {
var messages: [ChatMessage]?
var participants: [User]?
}
class User: Codable {
init (email: String) {
self.email = email
}
// system
var id: String?
var verifiedaccount: Int?
var rejected: Int?
...
}
I've further got the var conversationDetails = ConversationDetails () and I'm populating it with an API call. That all works fine.
I'd like to map the participants array inconversationDetailsand access the id property of each participant like so:
let recipient_ids = self.conversationDetails.participants.map( { (participant) -> String in
return participant.id
})
In my understanding, map iterates over the entire participants array, which is an array of User objects and I can access each item via participant.
However, I get Value of type '[User]' has no member 'id' for return participant.id.
Where is my misunderstanding?
Your participants var is optional, so you need to add question mark to access array. Without it, you try to call map on optional.
This is working code:
let recipientIds = conversationDetails.participants?.map( { (participant) -> String in
return participant.id
})
or shorter:
let recipientIds = conversationDetails.participants?.map { $0.id }
Also, you can use compactMap to remove nils from recipientIds array and have [String] array instead of [String?]:
let recipientIds = conversationDetails.participants?.compactMap { $0.id }
The first problem is that participants is optional so you need to add a ? when accessing it
conversationDetails.participants?
then when mapping you should use compactMap since id is also an optional property
let recipient_ids = conversationDetails.participants?.compactMap { $0.id }
Another variant is to not have an optional array but instead initialize it to an empty array. This is actually a much better way to handle collection properties because you can have a cleaner code by initializing them to an empty collection
var participants = [User]()
and then do
let recipient_ids = conversationDetails.participants.compactMap { $0.id }

Realm query problem with sorted localized data

Consider the following Realm models:
class Fruit: Object {
#objc dynamic var name = ""
let localizations = List<Localization>()
/**
Returns the localized name of the fruit matching the preferred language of the app
or self.name if the fruit does not have a localization matching the user preferred language codes.
*/
var localizedName: String? {
guard !Locale.isPreferredLanguageDefaultAppLanguage else { return self.name }
let preferredLanguagesCodes = Locale.preferredLanguagesCodes
let localizations = preferredLanguagesCodes.compactMap({ languageCode in Array(self.localizations).filter({ $0.languageCode == languageCode }).first })
return localizations.first?.localizedName ?? self.name
}
}
class Localization: Object {
#objc dynamic var localizedName: String = ""
#objc dynamic var languageCode: String = ""
}
Let's say I have 2 fruits in my database (represented in JSON format for the sake of simplicity):
[
{
"name": "Apple",
"localizations": [
{
"localizedName": "Pomme",
"languageCode": "fr"
}
]
},
{
"name": "Banana",
"localizations": [
{
"localizedName": "Banane",
"languageCode": "fr"
}
]
}
]
Now I want to get all the fruits in my database, and sort them alphabetically by their localizedName.
var localizedNameSortingBlock: ((Fruit, Fruit) -> Bool) = {
guard let localizedNameA = $0.localizedName, let localizedNameB = $1.localizedName else { return false }
return localizedNameA.diacriticInsensitive < localizedNameB.diacriticInsensitive
}
let sortedFruits = Array(Realm().objects(Fruit.self)).sorted(by: localizedNameSortingBlock)
If the first preferred language of my device is "English", I get this:
Apple
Banana
If it's set to "French":
Banane
Pomme
It's quite simple, but this solution has a major inconvenient:
By casting the Results<Fruit> collection into an array, I'm loosing the ability to get live updates via Realm's notification token system.
The problem is, I can't sort using NSPredicate directly on the Results collection, because the localizedName property is a computed property, thus is ignored by Realm.
I thought about writing the localizedName value directly into the name property of the Fruit object, but doing so requires to loop through all fruits and change their name whenever the user change of preferred language. There must be a better way.
So my question is:
Is there a way to retrieve all the fruits in my database, get them sorted by their localizedName, without loosing the ability to receive batch updates from Realm?

How to retrieve the ‘List<myObject>' realm array to array from realm model class?

My realm model class look like
class RoomRealmModel : Object {
dynamic var id: String = ""
var details = List<RoomDetailRealmModel>()
func saveItem() {
do {
let realm = try Realm()
realm.beginWrite()
realm.add(self, update: true)
try realm.commitWrite()
} catch{}
}
}
class RoomDetailRealmModel : Object{
dynamic var detailId: String = ""
dynamic var displayText: String = ""
}
I want to retrieve 'details' from the following.
details = RLMArray<RoomDetailRealmModel> <0x600000114f40> (
[0] RoomDetailRealmModel {
text = hello;
Counters = 9;
ParentID = ;
detailId = 33;
displayText = hello ;
}
);
I always get empty like in my console
(lldb) po (destinationData?[index]?.details)!
List<RoomDetailRealmModel> <0x600000853620> (
)
I am updating ‘details’ list via realm update command. I always get realm array.But I want to retrieve array type from realm array.Please help me, how to solve this issue
If you want to obtain [myObject] instead of List you can do something like this:
var array: [myObject] = [myObject]()
for object in myObjectList {
array.append(object)
}
Where myObjectList is List.
You can simply create a regular Swift Array from a Realm List by calling the initializer of Array accepting a Sequence, since List conforms to the sequence protocol.
So you can simply do
let room = RoomRealmModel()
let roomDetailsArray = Array(room.details)

Swift diff realm.io without fetching it in advance

I was wondering if there is a possibility in realm.io (swift) to select all items from one "table" that are not in the other one.
Lets say you have 2 classes:
class A: Object {
dynamic var id: Int = 0
dynamic var text: String = ""
}
class B: Object {
dynamic var id: Int = 0
dynamic var value: Bool = false
}
Is it possible to get an result of items from A who's id is not present in B?
There is actually a very simple way to do this using NSPredicate on Realm filter API.
func fetch() throws -> [A] {
do {
// Create Realm
let realm = try Realm()
// Get B objects from Realm and put their IDs to [Int] array
let IdB: [Int] = realm.objects(B).map { $0.id }
// Create predicate
// Filter all items where property id is not present in array IdB
let predicateFilter = NSPredicate(format: "NOT (id IN %#)", IdB)
// Get all A objects from array using predicateFilter
let objectsA = realm.objects(A).filter(predicateFilter)
// Return the [A] array
return objectsA.map { $0 }
} catch {
// Throw an error if any
throw error
}
}
Also note that all objects from fetched using Realm are lazy loaded which means that this method is also very fast. From the documentation:
All queries (including queries and property access) are lazy in Realm. Data is only read when the properties are accessed.