Alex Kolsa
Alex Kolsa

Reputation: 15

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

How it looks in firebase

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.

Upvotes: 0

Views: 629

Answers (1)

rayaantaneja
rayaantaneja

Reputation: 1748

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!

Upvotes: 2

Related Questions