Reputation: 15
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.
Upvotes: 0
Views: 629
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
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