How to map a Firestore DocumentID to a RealmObject attribute? - swift

I'm trying to provide some data in the cloud with Firestore that can be downloaded and stored in a Realm database on an iOS device.
The structure of my object that I want to store is:
import Foundation
import RealmSwift
import FirebaseFirestore
import FirebaseFirestoreSwift
#objcMembers class Flashcard: Object, Codable{
#objc dynamic var id: String? = NSUUID().uuidString
#objc dynamic var character: String?
#objc dynamic var title: String?
#objc dynamic var translation: String?
#objc dynamic var created: Date = Date()
let deck = LinkingObjects<FlashcardDeck>(fromType: FlashcardDeck.self, property: "cards")
override class func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case character
case translation
case created
case title
}
If I try to map the documentID to my id attribute with
#DocumentID #objc dynamic var id: String? = NSUUID().uuidString
If get the following error:
'Primary key property 'id' does not exist on object 'Flashcard'
How can I solve this problem?
EDIT: To make it more understandable here is a screenshot of my Firestore database:
The collection "PredifinedDecks" will store many decks. For example the id = DF59B1B3-BD22-47CE-81A6-04E7A274B98F represents one deck. Each deck will store an array/List with cards in it.

Not sure I fully understand the question but let me address this at a high level.
It appears there is a PredefinedDecks (a collection) that contains documents. Each document has a field (an array) of cards and some other field data. If the goal is to read in all of the documents (the decks) and their child data and store them as Realm objects, here's one solution. Start with a Realm object to hold the data from Firestore
class DeckClass: Object {
#objc dynamic var deck_id = ""
#objc dynamic var created = ""
#objc dynamic var title = ""
let cardList = List<CardClass>()
convenience init(withDoc: QueryDocumentSnapshot) {
self.init()
self.deck_id = withDoc.documentID
self.title = withDoc.get("title") as? String ?? "no title"
self.created = withDoc.get("created") as? String ?? "no date"
let cardArray = withDoc.get("cards") as? [String]
for card in cardArray {
let card = CardClass(withCard: card) {
self.cardList.append(card)
}
}
}
}
With this, you simply pass the documentSnapshot from Firestore for each document and the class will populate its properties accordingly.
and the code to read Firestore
func readDecks() {
let decksCollection = self.db.collection("PredefinedDecks")
decksCollection.getDocuments(completion: { documentSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
for doc in documentSnapshot!.documents {
let deck = DeckClass(withDoc: doc)
self.decksList.append(deck) //a Realm List class object? Something else?
}
})
}

Related

How to get Realm Objects by ID only?

I set up a Realm model (in Swift) with a children relationship:
import Foundation
import RealmSwift
class MyObject1: RealmSwift.Object, RealmSwift.ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: RealmSwift.ObjectId
#Persisted var childrenIDs = RealmSwift.List<String>()
}
I added the relationship as a RealmSwift.List of Strings, because I intent to create other model classes, each with an ID, that can be added as children. In other words, the children might not be of a single type/class. For example:
import Foundation
import RealmSwift
class MyObject2: RealmSwift.Object, RealmSwift.ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: RealmSwift.ObjectId
#Persisted var title: String
}
Now at some point, I have to fetch all the children by their ID, for example to show them in a list. But I do not know how. I know the realm.objects(Class.self).filter but this expects a single class type. So my question is, how can I fetch objects from a Realm only by their ID (ie without their class/type)?
As mentioned in a comment, ObjectId's are generic and have no correlation to the object class they are tied to. So you can't do exactly what you want.
But... there are options.
TL;DR
create another object with properties to hold the id and the object type, defined by an enum or
Use AnyRealmValue to store the objects themselves
Long answers:
Long answer #1
Create a class to store the id and type. Suppose we have a database that stores wine grape types; some are white and some are red but you want to store them all in the same list. Let me set up an example
enum GrapeTypesEnum: String, PersistableEnum {
case white
case red
}
class Grape: Object {
#Persisted(primaryKey: true) var _id: RealmSwift.ObjectId
#Persisted var grape_name = ""
#Persisted var grape_type: GrapeTypesEnum
}
class GrapeObjects: Object { //contains the grape id string & grape type
#Persisted var child_id = ""
#Persisted var child_type: GrapeTypesEnum
}
so then the model would be
class MyObject1: RealmSwift.Object {
#Persisted(primaryKey: true) var _id: RealmSwift.ObjectId
#Persisted var childGrapes = RealmSwift.List<GrapeObjects>()
}
You could then create a couple of grapes
let whiteGrape = Grape()
whiteGrape.name = "Chenin Blanc"
whiteGrape.grape_type = .white
let redGrape = Grape()
redGrape.name = "Cabernet Franc"
redGrape.grape_type = .red
let grape0 = GrapeObjects()
grape0.grape_id = whiteGrape._id.stringValue()
grape0.grape_type = whiteGrape.grape_type
let grape1 = GrapeObjects()
grape1.grape_id = redGrape._id.stringValue()
grape1.grape_type = redGrape.grape_type
then finally store those objects in your class. Those can then be sorted, filtered and you will know which ID goes with what type of object
let anObject = MyObject1()
anObject.childGrapes.append(objectsIn: [grape0, grape1])
Long answer #2
Another option is to not store the objectID string but store the actual objects by leveraging AnyRealmValue
class MyClass: Object {
#Persisted var myList = List<AnyRealmValue>()
}
let red = WhiteGrape()
let white = RedGrape()
let obj0: AnyRealmValue = .object(red) //both of these are objects,
let obj1: AnyRealmValue = .object(white) // even through they are different objects
let m = MyClass()
m.myGrapeList.append(obj0)
m.myGrapeList.append(obj1)
Then you can take action based on the the objects type
for grape in m.myGrapeList {
if let white = grape.object(WhiteGrape.self) {
print("is a white grape")
} else if let red = grape.object(RedGrape.self) {
print("is a red grape")
}
}

How to add custom data instances to an array in Firestore?

I have a question where and how to properly save users who are logged into a rooms, now I save them inside the collection "rooms" but I save the user as a reference
func addUserToRoom(room: String, id: String) {
COLLETCTION_ROOMS.document(room).updateData(["members": FieldValue.arrayUnion([COLLETCTION_USERS.document(id)])])
}
But when I create a rooms, I need to pass in an array of users
func fetchRooms() {
Firestore.firestore()
.collection("rooms")
.addSnapshotListener { snapshot, _ in
guard let rooms = snapshot?.documents else { return }
self.rooms = rooms.map({ (queryDocumentSnapshot) -> VoiceRoom in
let data = queryDocumentSnapshot.data()
let query = queryDocumentSnapshot.get("members") as? [DocumentReference]
// ======== I'm making a request here, but I don't understand how to save users next
query.map { result in
result.map { user in
let user = user.getDocument { userSnapshot, _ in
let data = userSnapshot?.data()
guard let user = data else { return }
let id = user["id"] as? String ?? ""
let first_name = user["first_name"] as? String ?? ""
let last_name = user["last_name"] as? String ?? ""
let profile = Profile(id: id,
first_name: first_name,
last_name: last_name)
}
}
}
let id = data["id"] as? String ?? ""
let title = data["title"] as? String ?? ""
let description = data["description"] as? String ?? ""
let members = [Profile(id: "1", first_name: "Test1", last_name: "Test1")]
return VoiceRoom(id: id,
title: title,
description: description,
members: members
})
}
}
This is how my room and profile model looks like
struct VoiceRoom: Identifiable, Decodable {
var id: String
var title: String
var description: String
var members: [Profile]?
}
struct Profile: Identifiable, Decodable {
var id: String
var first_name: String?
var last_name: String?
}
Maybe someone can tell me if I am not saving users correctly and I need to do it in a separate collection, so far I have not found a solution, I would be grateful for any advice.
I feel like this answer is going to blow your mind:
struct VoiceRoom: Identifiable, Codable {
var id: String
var title: String
var description: String
var members: [Profile]?
}
struct Profile: Identifiable, Codable {
var id: String
var first_name: String?
var last_name: String?
}
final class RoomRepository: ObservableObject {
#Published var rooms: [VoiceRoom] = []
private let db = Firestore.firestore()
private var listener: ListenerRegistration?
func addUserToRoom(room: VoiceRoom, user: Profile) {
let docRef = COLLECTION_ROOMS.document(room.id)
let userData = try! Firestore.Encoder().encode(user)
docRef.updateData(["members" : FieldValue.arrayUnion([userData])])
}
func fetchRooms() {
listener = db.collection("rooms").addSnapshotListener { snapshot, _ in
guard let roomDocuments = snapshot?.documents else { return }
self.rooms = roomDocuments.compactMap { try? $0.data(as: VoiceRoom.self) }
}
}
}
And that's it. This is all it takes to correctly store members of a room and simply decode a stored room into an instance of VoiceRoom. All of it is pretty self explanatory but if you have any questions feel free to ask it in the comments.
P.S. I changed COLLETCTION_ROOMS to COLLECTION_ROOMS
Edit:
I decided to elaborate on my changes anyway so that people who just started coding can understand how I reduced the code to just a few lines (skip this if you understood my code).
When your custom data instances conform to the Codable protocol it allows those instances to be automatically transformed into data the database can store (known as Encoding) AND allows them to converted back into the concrete types in your code when you retrieve them from the database (known as Decoding). And the magic of it is all you need to do is add : Codable to your structs and classes to get all this functionality for free!
Now, what about the functions? The addUserToRoom(room:user:) function takes in a VoiceRoom instead of a String because it's generally easier for the caller to just pass in room instead of room.id. This extra step can be done by the function itself and saves a little bit of clutter in your Views while also making it easier.
Finally the fetchRooms() function attaches a listener to the "rooms" collection and fires a block of code any time the collection changes in any way. In the block of code, I check if the document snapshot actually contains documents by using guard let roomDocuments = snapshot?.documents else { return }. After I know I have the documents all that's left to do is convert those documents back into VoiceRoom instances which can easily be done because VoiceRoom conforms to Codable (Remember how I said: "AND allows them to converted back into the concrete types in your code when you retrieve them from the database"). I do this by mapping the array of [QueryDocumentSnapshot] i.e. roomDocuments, into concrete VoiceRoom instances. I use a compact map because if any of the documents fail to decode it won't be contained in self.rooms.
So
try? $0.data(as: VoiceRoom.self)
tries to take every document (represented by $0) and represent its "data" "as" "VoiceRoom" instances.
And that's all it takes!

DocumentId wrapper is not working properly

I am using #DocumentId in my data struct:
struct UserProfile : Codable, Identifiable {
#DocumentID var id: String? = UUID().uuidString
}
When I try to access the id I get a random string of letters (04F9C67E-C4A5-4870-9C22-F52C7F543AA5) Instead of the name of the document in firestore (GeG23o4GNJt5CrKEf3RS)
To access the documentId I am using the following code in an ObservableObject:
class Authenticator: ObservableObject {
#Published var currentUser: UserProfile = UserProfile()
#Published var user: String = ""
func getCurrentUser(viewModel: UsersViewModel) -> String {
guard let userID = Auth.auth().currentUser?.uid else {
return ""
}
viewModel.users.forEach { i in
if (i.userId == userID) {
currentUser = i
}
}
print("userId \(currentUser.id ?? "no Id")")
print("name \(currentUser.name)")
return userID
}
Where currentUser is a UserProfile struct. currentUser.name returns the proper value. What am I doing wrong?
currentUser is a member of an array populated using the following method:
func fetchData() {
db.collection("Users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.compactMap { (queryDocumentSnapshot) -> UserProfile? in
return try? queryDocumentSnapshot.data(as: UserProfile.self)
}
}
}
The id of the UserProfile struct and the id of your data in Firestore are separate. That's why you're seeing the random string of letters.
I would change your struct similar to the below and you can just ignore the regular "id" variable when you initialize and refer to the documentID instead.
struct UserProfile : Codable, Identifiable {
var id = UUID() // ID for the struct
var documentID: String // ID for the post in Database
}
The problem is that you're overwriting the fetched DocumentID with a newly generated UUID string. You should keep it uninitialized, there is no need to set an ID manually. Even if there is, then you should set it only if the object is created from code but not if it gets decoded from Firestore
found my specific problem, I defined CodingKeys enum and when you do that, you have to make sure that you have a mapping for every attribute.
make sure you put all of your model attributes in CodingKeys enum or don't use custom CodingKeys at all
EDIT: taking the above back...it's still buggy...maybe decodes fine but not encodes and uploading
my workaround for now is to not use .adddocument but instead .setdataenter image description here

Can't save string to Realm Swift

I'm trying to save a string to Realm. For some reason, a nil value is being saved to Realm. Here is the function where I save to Realm (imageKey is the String)
func saveImageKey(_ imageKey: Data){
do{
try realm.write{
realm.add(imageKey)
}
}
catch{
print("Error saving imageKey: \(error)")
}
}
I'm not sure if this affects Realm, but when initializing the imageKey variable, I used a didSet to reorganize the imageKey objects by date created as follows:
var imageKey: Results<Data>?{
didSet{ //every time imageKey is updated, this will be called
imageKey = imageKey!.sorted(byKeyPath: "date", ascending: false)
print(imageKey)
}
}
This is how I converted the imageKey string into a Data object:
let newData = Data()
newData.imageKey = "item" + String(textCount)
saveImageKey(newData)
This is the class definition for Data:
class Data: Object{
#objc dynamic var text: String = ""
#objc dynamic var imageKey: String = ""
#objc dynamic var date = NSDate().timeIntervalSince1970
}
I have already checked for common errors, such as not initializing Realm, not adding the dynamic to the variable declarations in the Data class.
What could the issue be? Please let me know if you need more code/information.

How to create one Realm class from several other classes?

Good afternoon everyone,
Im currently working on a task that called "Bookmark". The short description is whenever i clicked the bookmark button, app will save the article and then display it in the BookmarkVC.
I have 3 types of object called "News", "Documents" and "ITSectionResult" and my idea is to create an object called "BookmarkItem" which contain element of 3 above objects and one property called bookmarkCategory to indicate type of objects, so that i can use it to display in BookmarkVC. I just want to work on only one realm object, so any one can help me an an idea to group these guys together?. Here i would attach my draft code as below:
For News class:
class NewsArticle:Object {
dynamic var title: String?
dynamic var body:String?
dynamic var category:String?
dynamic var createTime:String?
dynamic var image:String?
dynamic var id:String?
convenience init (title:String, body:String, category:String,
image:String, id:String, createTime:String) {
self.init()
self.title = title
self.body = body
self.category = category
self.image = image
self.id = id
self.createTime = createTime
}
Document class:
class Documents {
var id: String?
var title:String?
var createTime:CLong?
var url:String?
init (title:String, id:String, createTime:CLong?, url:String) {
self.title = title
self.id = id
self.createTime = createTime
self.url = url
}
ITSectionResult class:
class SectionSearchResult {
var title:String?
var image:String?
var id:String?
var byCategory:String?
init (title:String, image:String, id:String, byCategory:String) {
self.title = title
self.image = image
self.id = id
self.byCategory = byCategory
}
and finally the drafting BookmarkItem class:
class BookmarkItem:Object {
//Category
dynamic var bookmarkCategory:BookMarkItemCategory?
dynamic var title: NewsArticle?
dynamic var body:NewsArticle?
dynamic var category:NewsArticle?
dynamic var createTime:NewsArticle?
dynamic var image:NewsArticle?
dynamic var id:NewsArticle?
dynamic var link:String?
dynamic var url:String?
}
class BookMarkItemCategory:Object {
dynamic var name = ""
}
Here i would remind that, the BookmarkItem class uses 3 major properties to display in the BookmarkVC, "image" for filter category type(example: book image for Documents object, newspaper icon for News object) , "title" for the title and the url for display in WebView. Thank you so much, and wish you guys have a good weekend ahead.
As mentioned above in the comments by EpicPandaForce, this can be achieved by not having a class for each type and instead use a unified model.
import RealmSwift
final class UnifiedModel: Object {
dynamic var title: String = ""
dynamic var id: String = ""
dynamic var createTime: String = ""
dynamic var category: String = ""
dynamic var body: String?
dynamic var image: String?
override static func primaryKey() -> String {
return "id"
}
}
With the model I've shown above, every instance would need a title, id, create time and category while body and image could be left nil. Please feel free to follow-up if you have any other questions.
Disclaimer: I work for Realm.