How to get an Dictionary from Firebase Firestore in Swift - 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"]!)")
}

Related

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

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)
})
}

How to retrieve data from Firestore as soon as SwiftUI view appears?

I have this class CurrentUser that manages the currently logged in user and pulls the data for that user from Firebase.
One of CurrentUser's attributes is userEventIDs. I also have a collection of Events documents. Each user has their own array of event IDs that correspond to the events within the Events collection in my Firestore database.
On the MyAccount view struct I have an onAppear method that queries the Events collection based on the currentUser's array of eventIds, returns those Events, and then sorts them to be either before or after today based on the date of the event.
Currently the eventIds load in the first time this view is opened, but the query from the events comes back blank twice and only after the view is switched to another one and back to the MyAccount view will the page populate with these events.
Is there something I can do to make the events load on the first time the view is opened?
CurrentUser
class CurrentUser: ObservableObject {
let user = Auth.auth().currentUser
#Published var currentUserInformation = User(id: "", name: "", email: "'", accountType: "", profPicURL: "", coverPhotoURL: "", numberFollowers: nil, description: nil, location: nil, websiteLink: nil, orgID: nil, userEventIDs: [String](), userEvents: [Event]())
init() {
getUserInformation()
}
func getUserInformation() {
let UID = user!.uid
let database = Firestore.firestore()
database.collection("Organizers").whereField("Organizer ID", isEqualTo: UID).getDocuments() { (querySnapshot, err) in
if err != nil {
print("Error getting documents: \(err!)")
}
for document in querySnapshot!.documents {
self.currentUserInformation.id = document.documentID
self.currentUserInformation.name = document.get("Organization Name") as! String
self.currentUserInformation.email = document.get("Email") as! String
self.currentUserInformation.accountType = document.get("Account Type") as! String
self.currentUserInformation.profPicURL = document.get("Profile Pic URL") as! String
self.currentUserInformation.coverPhotoURL = document.get("Cover Pic URL") as! String
self.currentUserInformation.numberFollowers = (document.get("Number of Followers") as! Int)
self.currentUserInformation.description = (document.get("Organization Description") as! String)
self.currentUserInformation.websiteLink = (document.get("Organization Website Link") as! String)
self.currentUserInformation.location = (document.get("Organization Location") as! String)
self.currentUserInformation.orgID = (document.get("Organizer ID") as! String)
self.currentUserInformation.userEventIDs = (document.get("Events") as! [String])
self.currentUserInformation.accountType = "Organizer"
}
}
if self.currentUserInformation.id == "" {
database.collection("Activists").whereField("UID", isEqualTo: UID).getDocuments() { (querySnapshot, err) in
if err != nil {
print("Error getting documents: \(err!)")
}
for document in querySnapshot!.documents {
self.currentUserInformation.id = document.documentID
let firstName = document.get("First Name") as! String
let lastName = document.get("Last Name") as! String
self.currentUserInformation.name = "\(firstName) \(lastName)"
self.currentUserInformation.email = document.get("Email") as! String
self.currentUserInformation.accountType = "Activist"
self.currentUserInformation.profPicURL = document.get("Profile Pic") as! String
self.currentUserInformation.userEventIDs = (document.get("Events") as! [String])
}
}
}
}
func getUserEvents() {
let database = Firestore.firestore()
let eventRef = database.collection("Events")
for eventID in self.currentUserInformation.userEventIDs {
for event in self.currentUserInformation.userEvents {
if event.id == eventID {
break
}
}
eventRef.document(eventID).getDocument() { (document, error) in
if let document = document {
let id = document.documentID
let eventTitle = document.get("Name") as! String
let organizer = document.get("Organizer") as! String
let organizerID = document.get("Organizer ID") as! String
let eventDescription = document.get("Description") as! String
let date = document.get("Date") as! String
let time = document.get("Time") as! String
let location = document.get("Location") as! String
let numAttending = document.get("Number Attending") as! Int
let eventPhotoURL = document.get("Event Photo URL") as! String
self.currentUserInformation.userEvents.append(Event(id: id, eventTitle: eventTitle, eventOrganizer: organizer, eventOrganizerID: organizerID, eventDescription: eventDescription, date: date, time: time, location: location, numAttending: numAttending, eventPhotoURL: eventPhotoURL))
} else {
print("Document does not exist")
}
}
}
}
}
View
.onAppear() {
if currentActivist.currentUserInformation.userEvents.count != currentActivist.currentUserInformation.userEventIDs.count {
currentActivist.getUserEvents()
print("Getting user events")
}
pastEvents = MyAccountActivistView.getSortedEvent(actEvents: currentActivist.currentUserInformation.userEvents)["Past"]!
futureEvents = MyAccountActivistView.getSortedEvent(actEvents: currentActivist.currentUserInformation.userEvents)["Upcoming"]!
}
A couple of quick notes:
Most Firebase calls are asynchronous (check out this article to understand why), so your call to Auth.auth().currentUser is most likely going to return nil. Instead, you should register an AuthenticationStateListener. See this sample code to see how it's done.
Instead of instantiating an empty User instance, make currentUserInformation optional
Mapping data is much easier using Firestore's support for Codable. I've written extensively about this, but the gist of it is, you'll be able to map documents with a single line of code (instead of having to manually map every single field). The Firestore documentation actually has a nice code snippet that you can adopt:
let docRef = db.collection("cities").document("BJ")
docRef.getDocument { (document, error) in
// Construct a Result type to encapsulate deserialization errors or
// successful deserialization. Note that if there is no error thrown
// the value may still be `nil`, indicating a successful deserialization
// of a value that does not exist.
//
// There are thus three cases to handle, which Swift lets us describe
// nicely with built-in Result types:
//
// Result
// /\
// Error Optional<City>
// /\
// Nil City
let result = Result {
try document?.data(as: City.self)
}
switch result {
case .success(let city):
if let city = city {
// A `City` value was successfully initialized from the DocumentSnapshot.
print("City: \(city)")
} else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
print("Document does not exist")
}
case .failure(let error):
// A `City` value could not be initialized from the DocumentSnapshot.
print("Error decoding city: \(error)")
}
}
Avoid force unwrapping (using the ! operator), use optional unwrapping (using the ? operator), and the nil-coalescing operator (??) instead

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.

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.

Cannot subscript a value of type '[Any]' with an index of type 'String'

I am retrieving data from firestore and so far everything is successful. This is the result in my console.
[{
Name = Tuna;
Price = "$3.6"; }, {
Name = Snapper;
Price = "$25.60"; }]
I want to store Name and Price into variables so that I can display them in my table view. As shown in the code bellow when i set nameOfItem to the Data2, i get an error that states, "Cannot subscript a value of type '[Any]' with an index of type 'String'". I was wondering if someone can help me fix this!
override func viewWillAppear(_ animated: Bool) {
itemCollectionRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
}else {
guard let snap = snapshot else {return}
for document in snap.documents {
let data2 = document.data()["Items"]! as? Array ?? []
print(data2)
let RandomVariable = data2[0]
print(RandomVariable)
let nameOfItem = data2["Name"] as? String ?? ""
let priceOfItem = data2["Price"] as? String ?? ""
//let priceOfItem = data2["Price"] as? String ?? ""
//print(nameOfItem, priceOfItem)
}
}
}
As you can see for yourself there is no such thing as an "Items" element in your console output instead as the error message says you have an array so change your code inside the for loop to
let data = document.data() as? [Any] ?? []
for item in data {
if let dictionary = item as? [String: String] {
let nameOfItem = dictionary["Name"] ?? ""
let priceOfItem = dictionary["Price"] ?? ""
//...
}
}