Get data from firestore and assign it to an array of dictionaries - swift

I am trying to get data from firestore collection and assign it to an array of dictionaries. for this part of the code below... i get the error "Cast from 'QuerySnapshot?' to unrelated type '[[String : Any]]' always fails" and the console prints "is not working".
guard let snap = snapshot as? [[String:Any]] else {
print("is not working")
completion(.failure(DatabaseError.failedToFetch))
return
}
Here is the full code.
// fetches and returns all conversations for the user with passed in uid
public func getAllConversations(for uid: String, completion: #escaping(Result<[Conversation], Error>) -> Void) {
print("fetching all convos")
//NEW
let db = Firestore.firestore()
let CurrentUser = Auth.auth().currentUser?.uid
let ListRef = db.collection("users").document(CurrentUser!).collection("conversations")
// fetch the current users convo list
ListRef.getDocuments { snapshot, error in
if let err = error {
debugPrint("Error fetching documents: \(err)")
} else {
guard let snap = snapshot as? [[String:Any]] else {
print("is not working")
completion(.failure(DatabaseError.failedToFetch))
return
}
print("is working")
let conversations: [Conversation] = snap.compactMap({ dictionary in
guard let id = dictionary["id"] as? String,
let name = dictionary["name"] as? String,
let otherUserUID = dictionary["other_user-uid"] as? String,
let latestMessage = dictionary["latest-message"] as? [String:Any],
let date = latestMessage["date"] as? String,
let message = latestMessage["message"] as? String,
let isRead = latestMessage["is-read"] as? Bool else {
return nil
}
//save other user ID to a global var
self.test = otherUserUID
//assign data into an array of dictionaries
let latestConvoObject = LatestMessage(date: date, text: message, isRead: isRead)
return Conversation(id: id, name: name, otherUserUid: otherUserUID, latestMessage: latestConvoObject)
})
completion(.success(conversations))
}
}
}

There are a numbers of way to read that data, and the process can be simplified by conforming objects to the codable protocol but let me provide a straight forward example. I don't know what your Conversation object looks like so here's mine
class ConversationClass {
var from = ""
var to = ""
var msg = ""
var timestamp = 0
convenience init(withDoc: DocumentSnapshot) {
self.init()
self.from = withDoc.get("from") as? String ?? "no from"
self.to = withDoc.get("to") as? String ?? "no to"
self.msg = withDoc.get("msg") as? String ?? "no msg"
self.timestamp = withDoc.get("timestamp") as? Int ?? 0
}
}
and then here's the the code that reads in all the conversation documents from a Collection, stores each in a ConversationClass object, puts those in an array and returns it through an escaping completion handler
func getConversations(completion: #escaping( [ConversationClass] ) -> Void) {
let conversationCollection = self.db.collection("conversations")
conversationCollection.getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
var convoArray = [ConversationClass]()
for doc in docs {
let convo = ConversationClass(withDoc: doc)
convoArray.append(convo)
}
completion(convoArray)
})
}

Related

Fetched data from Firestore returns duplicates

I am trying to fetch data from firebase firestore. The problem i have is that my fetch is returning the results x4 times. For example when i do print(name) it print the users name x4 times.
I think there may be a loop that is not working correctly?
// fetches and returns all conversations for the user with passed in uid
public func getAllConversations(for uid: String, completion: #escaping(Result<[Conversation], Error>) -> Void) {
let CurrentUser = Auth.auth().currentUser?.uid
let db = Firestore.firestore()
let ConversationRef = db.collection("users").document(CurrentUser!).collection("conversations").document(
"jVymlfbpuAYQQ9Brf8SbUZ7KCGg1")
// get the otherUserUId TO DO
ConversationRef.getDocument { snapshot, error in
if error != nil {
print("Error connecting to database")
} else {
if let document = snapshot {
if document.exists {
let data = document.data()
print(data)
let conversations: [Conversation] = data!.compactMap ({ dictionary in
guard let conversationId = data!["id"] as? String,
let name = data!["name"] as? String,
let otherUserUid = data!["other_user-uid"] as? String,
let latestMessage = data!["latest-message"] as? [String:Any],
let date = latestMessage["date"] as? String,
let message = latestMessage["message"] as? String,
let isRead = latestMessage["is-read"] as? Bool
else {
return nil
}
print(name)
let latestMessageObject = LatestMessage(date: date, text: message, isRead: isRead)
return Conversation(id: conversationId, name: name, otherUserUid: otherUserUid, latestMessage: latestMessageObject)
})
completion(.success(conversations))
}
else {
completion(.failure(DatabaseError.failedToFetch))
return
}
}
}
}
}
Please note that ConversationRef.getDocument{..} will only Return One Specific Document, which you’re Referring here :
let ConversationRef = db.collection("users").document(CurrentUser!).collection("conversations").document("jVymlfbpuAYQQ9Brf8SbUZ7KCGg1”)
So the let data = document.data()
will be single [String:Any] object(in this case Single ‘Conversation’),
not the Array of Dictionaries(eg: [Conversations]).
Try doing it this way:
// fetches and returns all conversations for the user with passed in uid
public func getAllConversations(for uid: String, completion: #escaping(Result<Conversation, Error>) -> Void) {
let CurrentUser = Auth.auth().currentUser?.uid
let db = Firestore.firestore()
let ConversationRef = db.collection("users").document(CurrentUser!).collection("conversations").document(
"jVymlfbpuAYQQ9Brf8SbUZ7KCGg1")
// get the otherUserUId TO DO
ConversationRef.getDocument { snapshot, error in
if error != nil {
print("Error connecting to database")
} else {
if let document = snapshot {
if document.exists {
if let data = document.data() {
if let conversationId = data["id"] as? String,
let name = data["name"] as? String,
let otherUserUid = data["other_user-uid"] as? String,
let latestMessage = data["latest-message"] as? [String:Any],
let date = latestMessage["date"] as? String,
let message = latestMessage["message"] as? String,
let isRead = latestMessage["is-read"] as? Bool {
print(name)
let latestMessageObject = LatestMessage(date: date, text: message, isRead: isRead)
let conversations = Conversation(id: conversationId, name: name, otherUserUid: otherUserUid, latestMessage: latestMessageObject)
completion(.success(conversations))
}
}
}
else {
completion(.failure(DatabaseError.failedToFetch))
return
}
}
}
}
}
// fetches and returns all conversations for the user with passed in uid
public func getAllConversations(for uid: String, completion: #escaping(Result<[Conversation], Error>) -> Void) {
let CurrentUser = Auth.auth().currentUser?.uid
let db = Firestore.firestore()
let ConversationRef = db.collection("users").document(CurrentUser!).collection("conversations")
ConversationRef.addSnapshotListener { snapshot, error in
if error != nil {
print("Error connecting to database")
} else {
guard let snap = snapshot else {
completion(.failure(DatabaseError.failedToFetch))
return
}
for document in snap.documents {
let data = document.data()
print(data)
guard let conversationId = data["id"] as? String,
let name = data["name"] as? String,
let otherUserUid = data["other_user-uid"] as? String,
let latestMessage = data["latest-message"] as? [String:Any],
let date = latestMessage["date"] as? String,
let message = latestMessage["message"] as? String,
let isRead = latestMessage["is-read"] as? Bool else {
return
}
print(name)
let latestMessageObject = LatestMessage(date: date, text: message, isRead: isRead)
let conversations = [Conversation(id: conversationId, name: name, otherUserUid: otherUserUid, latestMessage: latestMessageObject)]
completion(.success(conversations))
}
}
}
}

How to get an Dictionary from Firebase Firestore in Swift

I wonder how to get an Dictionary from my Firestore. For normal Arrays I've done it like that:
func returnArray(){
let newpath = Firestore.firestore().collection(path)
newpath.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.Array = documents.map { (queryDocumentSnapshot) -> String in
let data = queryDocumentSnapshot.data()
let Name = data["myField"] as? String ?? ""
return Name
}
}
}
That works perfectly fine. My question is now how I have to change my code that its getting an Array out of the Firebase. I had an idea, but it doesn't work:
func returnDictionary(){
let newpath = Firestore.firestore().collection(path)
newpath.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.Dictionary = documents.map { (queryDocumentSnapshot) -> // I don't know what to fill here
in
let data = queryDocumentSnapshot.data()
let Name = data["AnzeigeName"] as? String ?? ""
let ID = data["selfID"] as? String ?? ""
return ID: Name
}
}
}
What can I try next?
.map is going to give you an array, but you can use Dictionary's init(uniqueKeysWithValues:) to turn this into a Dictionary:
let dictionary : Dictionary<String,String> = .init(uniqueKeysWithValues: documents.compactMap { queryDocumentSnapshot -> (String,String)? in
let data = queryDocumentSnapshot.data()
if let name = data["AnzeigeName"] as? String, let id = data["selfID"] as? String {
return (id, name)
}
return nil
})
It's important to know that uniqueKeysWithValues will crash if the keys are not in fact unique, so you'd want to check for that first (look at #New Dev's comment about init(_:uniquingKeysWith:) to handle this). I'm also using compactMap to get rid of nil values.
(Note: In Swift, generally variable names are lowercased and type names are uppercased. Going against that pattern can make your code challenging to read for others)
Firestore Example
{
name: "Anonymous"
pet {
name: "Max"
age: "3"
}
}
Query
Firebase.firestore().collection(path).getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
} else {
for document in querySnapshot!.documents {
let data = document.data()
let name = data["name"]
let pet = data["pet"] as! Dictionary<String, String>
print("Name: \(name)")
print("Pet-name: \(pet["name"]!)")
print("Pet-age: \(pet["age"]!)")
}

Setting up Nested Structs and populating via FireStore [Swift]

I have two structs, one nested within the other:
struct User {
let uid: String
let name: String
var pack: [Doggo]
}
struct Doggo {
let dogUid: String
let dogName: String
let dogBreed: String
let dogBorn: String
let dogProfileImageURL: String
}
Is the following code the proper way to access the firebase data for each?
guard let userUid = Auth.auth().currentUser?.uid else { return }
let userRef = self.db.collection("users").document(userUid)
let packRef = self.db.collection("users").document(userUid).collection("pack")
userRef.getDocument { (document, error) in
if let document = document, document.exists {
let data = document.data()
let name = data?["name"] as? String ?? "Anonymous"
packRef.getDocuments(completion: { (snapshot, error) in
if let err = error {
debugPrint("Error fetchings docs: \(err)")
} else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let dogUid = data["dogUid"] as? String ?? "No dogUid"
let dogName = data["dogName"] as? String ?? "No dogName"
let dogBreed = data["dogBreed"] as? String ?? "No dogBreed"
let dogBorn = data["dogBorn"] as? String ?? "No dogBorn"
let dogProfileImageURL = data["dogProfileImageURL"] as? String ?? "No dogProfileImageURL"
let newDoggo = Doggo(dogUid: dogUid, dogName: dogName, dogBreed: dogBreed, dogBorn: dogBorn, dogProfileImageURL: dogProfileImageURL)
self.doggos.append(newDoggo)
print(newDoggo)
}
}
self.user = User(uid: userUid, name: name, pack: self.doggos)
print(self.user)
self.configureHomeController()
self.configureMenuController()
})
} else {
print("Document does not exist")
}
}
It appears to work the way I'd like it to, but I don't want to run into issues down the line as it's quite foundational to the rest of the app.

How do I loop through a firestore document that has an array of maps?

I mostly work with dictionaries since I am fairly new but here we have an embedded struct that I need to loop through. With this, I need to be able to populate expanding cells in UITableView.
My struct looks like this:
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: PlanDetails
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
And my query looks like this:
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
for document in documentSnapshot!.documents {
let eMail = document.get("E-mail")
let message = document.get("Message")
let timeStamp = document.get("Time_Stamp")
let userEmail = document.get("User_Email")
let planDetails = document.get("planDetails")
// how to loop through planDetails
}
}
I have gotten this far:
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
guard let planDetails = document.get("planDetails") as? [[String : Any]] else {
let email = document.get("E-mail")
let message = document.get("Message")
let timeStamp = document.get("Time_Stamp")
let userEmail = document.get("User_Email")
let status = false
}
for plan in planDetails {
if let menuItemName = plan["menuItemName"] as? String {
// not sure I populate the menuItemsName, menuItemsQuantity and menuItemsPrice within the array
}
}
}
}
}
UPDATE 2:
Final query: This is finally working as expected. But am not able to populate the expanding cells.
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let message = document.get("Message") as? String,
let timeStamp = document.get("Time_Stamp") as? String,
let userEmail = document.get("User_Email") as? String,
let status = document.get("status") as? Bool else {
continue
}
for plan in planDetails {
guard let menuItemName = plan["menuItemName"] as? String,
let menuItemQuantity = plan["menuItemQuantity"] as? Int,
let menuItemPrice = plan["menuItemPrice"] as? Double else {
continue
}
let planDetails = PlanDetails(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity, menuItemPrice: menuItemPrice)
let complaint = Complain(eMail: email, message: message, timeStamp: timeStamp, userEmail: userEmail, status: status, planDetails: planDetails)
self.messageArray.append(complaint)
print(self.messageArray)
}
}
}
}
EDIT
// OPTION 1 - FULL REQUIREMENT
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
// all of these properties are required to parse a single document
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let message = document.get("Message") as? String,
let timeStamp = document.get("Time_Stamp") as? String,
let userEmail = document.get("User_Email") as? String,
let status = document.get("status") as? Bool else {
continue // continue this loop
}
for plan in planDetails {
// all of these properties are required to instantiate a struct
guard let name = plan["menuItemName"] as? String,
let price = plan["menuItemName"] as? Double,
let quantity = plan["menuItemQuantity"] as? Int else {
continue // continue this loop
}
let plan = PlanDetails(menuItemName: name, menuItemQuantity: quantity, menuItemPrice: price)
// then you will likely append this plan to an array
}
}
}
}
// OPTION 2 - PARTIAL REQUIREMENT
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
// all of these properties are required to parse a single document
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let message = document.get("Message") as? String else {
continue // continue this loop
}
// these properties are not required
let timeStamp = document.get("Time_Stamp") as? String // optional (can be nil)
let userEmail = document.get("User_Email") as? String // optional (can be nil)
let status = document.get("status") as? Bool ?? false // not optional because it has a default value of false
for plan in planDetails {
// all of these properties are required to instantiate a struct
guard let name = plan["menuItemName"] as? String,
let price = plan["menuItemName"] as? Double,
let quantity = plan["menuItemQuantity"] as? Int else {
continue // continue this loop
}
let plan = PlanDetails(menuItemName: name, menuItemQuantity: quantity, menuItemPrice: price)
// then you will likely append this plan to an array
}
}
}
}
There are a number of other ways you can do this, these are just a couple. For example, you can instantiate the struct with the [String: Any] dictionary itself.
I finally figured out what the problem was. Had to do with the way I setup the struct and how I populate the array.
Right way is as such i.e. the planDetails need to be cast as an array:
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: [PlanDetails]
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
And then I need to iterate through each plan item, add it to the array and then add the array to the top level Complain:
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let documentSnapshot = documentSnapshot {
for document in documentSnapshot.documents {
guard let planDetails = document.get("planDetails") as? [[String : Any]],
let email = document.get("E-mail") as? String,
let status = document.get("status") as? Bool else {
continue
}
for plan in planDetails {
guard let menuItemName = plan["menuItemName"] as? String,
let menuItemQuantity = plan["menuItemQuantity"] as? Int else {
continue
}
let singlePlan = PlanDetails(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity)
self.planDetailsArray.append(singlePlan)
}
let complain = Complain(eMail: email, status: status, planDetails: self.planDetailsArray)
self.planDetailsArray.removeAll()
self.messageArray.append(complain)
}
}
// Print the main array or load table
}
}
bsod's answer stays as the correct answer because without his help, I would not have been able to arrive at his conclusion.

Always getting nil in completion

I'm trying to get Map data I have in Firestore, this is how it looks:
I'm trying to get the data, and create an array of Friend Object and return the array in the completion handler.
This is what I have:
func fetchFriendList(_ id: String, completion: #escaping([Friend]?)->()) {
var fetchedFriends: [Friend]?
db.collection(USERS_COLLECTION).document(id).getDocument { (doc, err) in
if err == nil && doc != nil {
guard let results = doc?.data()?[USER_FOLLOWING] as? [String: Any] else { return }
for result in results { // Getting the data in firebase
if let resultValue = result.value as? [String: Any] { // Getting only the value of the MAP data, we do not need the key.
//Getting the fields from the result
guard let id = resultValue[FRIEND_ID] as? String else { return }
guard let profilePic = resultValue[FRIEND_PROFILE_PIC] as? String else { return }
guard let username = resultValue[FRIEND_NAME] as? String else { return }
guard let email = resultValue[FRIEND_MAIL] as? String else { return }
//Creating a new Friend object from the fields
let friend = Friend(id: id, profilePicture: profilePic, username: username, email: email)
fetchedFriends?.append(friend)
}
completion(fetchedFriends)
}
}else {
print(err!.localizedDescription)
completion(nil)
}
}
}
I tried printing the results, and resultValue etc, they are not nil.
But, after trying to append and print the fetchedFriends Array, I get nil, and the completion is also nil.
I don't really understand why this is happening.
The problem is that you haven't initialized variable fetchedFriends and you have used optional type when appending data to it. Since it has not been initialized, it will skip appending to it. You should initialize it in the beginning. The updated code would be as follows.
func fetchFriendList(_ id: String, completion: #escaping([Friend]?)->()) {
var fetchedFriends: [Friend] = []
db.collection(USERS_COLLECTION).document(id).getDocument { (doc, err) in
if err == nil && doc != nil {
guard let results = doc?.data()?[USER_FOLLOWING] as? [String: Any] else { return }
for result in results { // Getting the data in firebase
if let resultValue = result.value as? [String: Any] { // Getting only the value of the MAP data, we do not need the key.
//Getting the fields from the result
guard let id = resultValue[FRIEND_ID] as? String else { return }
guard let profilePic = resultValue[FRIEND_PROFILE_PIC] as? String else { return }
guard let username = resultValue[FRIEND_NAME] as? String else { return }
guard let email = resultValue[FRIEND_MAIL] as? String else { return }
//Creating a new Friend object from the fields
let friend = Friend(id: id, profilePicture: profilePic, username: username, email: email)
fetchedFriends.append(friend)
}
completion(fetchedFriends)
}
}else {
print(err!.localizedDescription)
completion(nil)
}
}
}
Hope it helps.