Storing asynchronous Cloud Firestore query results in Swift - ios

I am working on a simple project using Swift 5, SwiftUI and Firebase which cycles through given ids in an array, searching in the Cloud Firestore database for each id, and appending the corresponding name associated with the id to a new array.
Here is a picture of my database:
For example, I am given an array a few ids, then for each element in the given array, I get the document associated with that id, then print the "firstname" field in that document.
However, I want to store each "firstname" value retrieved into a separate array locally for use later. In Javascript, I know this is done using await and async functions, but I found out through countless hours of troubleshooting that Swift doesn't have async or await.
Here is my code:
func convertToNames(arr: [String]) -> [String]{
var newArr : [String] = []
for id in arr {
let docRef = db.collection("users").document(id)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
let data = document.get("firstname") ?? "nil"
print("gotten data: \(data)")
newArr.append(String(describing: data))
} else {
print("Document does not exist")
}
}
}
print("NEW ARRAY: \(newArr)")
return (newArr)
}
This code prints an empty array when finished, and I understand why but I just have no clue how to make it work in Swift. I've spent about 5 hours today sifting through the Firebase documentation, looking at example code, and sifting through Youtube, but none of the resources address this issue to the extent that I need. If it is impossible to do, please let me know.

You need a dispatch group in addition to a completion
func convertToNames(arr: [String],completion:#escaping(([String]) -> ())) {
var newArr : [String] = []
let g = DispatchGroup()
for id in arr {
let docRef = db.collection("users").document(id)
g.enter()
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
let data = document.get("firstname") ?? "nil"
print("gotten data: \(data)")
newArr.append(String(describing: data))
g.leave()
} else {
print("Document does not exist")
g.leave()
}
}
}
g.notify(queue:.main) {
print("NEW ARRAY: \(newArr)")
completion(newArr)
}
}
Call
convertToNames(arr:<#arr#>) { res in
print(res)
}

Related

How to read the current users profile document in Firestore

I am working on my first IOS app using Firestore. I am successfully retrieving all user documents or a specific document with a document name from the "users" collection. But as soon as I want to query the currentUsers documents I have trouble. I need the data for a user profile viewController.
I can query a specific document and print it with this code:
func loadUserData() {
let userDocumentRef = Firestore.firestore().collection("users").document(
"6tIzsXgbOMDIFpdgR18i")
userDocumentRef.getDocument { (document, error) in
if let document = document, document.exists {
let userData = document.data().map(String.init(describing: )) ?? "nil"
print("Document data: \(userData)")
} else {
print("document does not exist")
}
}
}
But as soon as I try to get the currentUsers document with this code, then I get the error that "document does not exist":
func currentUserData() {
let userID : String = (Auth.auth().currentUser?.uid)!
let userRef = Firestore.firestore().collection("users").document(userID)
userRef.getDocument { (document, error) in
if let document = document, document.exists {
let userData = document.data().map(String.init(describing: )) ?? "nil"
print("Document data: \(userData)")
} else {
print("document does not exist")
}
}
}
Here is a screenshot of the users collection in Firestore.
Screenshot of Firestore users collection
I just simply cannot figure out what I am doing wrong, so any help is appreciated.
It seems like there is an issue when mapping the document. Here is how you can map data from Firestore to Swift:
struct UserProfile: Codable, Identifiable {
#DocumentID var id: String?
var userId: String
var firstName: String
// ... other attributes
}
func currentUserData() {
// Note: this is unsafe, please use an auth state change listener instead
// Also, please try to avoid using force unwrap - your app will crash if the attribute is nil
let userID : String = (Auth.auth().currentUser?.uid)!
let userRef = Firestore.firestore().collection("users").document(userID)
userRef.getDocument { (document, error) in
if let document = document, document.exists {
let userData = document.data(as: UserProfile.self)
print("Document data: \(userData)")
} else {
print("document does not exist")
}
}
}
Here is how to set up an auth state change listener: https://firebase.google.com/docs/auth/ios/start#listen_for_authentication_state

SwiftUI - assign Firbase Database output to Data Model

im new to swift and Firebase. And im trying to understand it.
I have a Cloud Firestore DB, where i store some user data like username and email. Now i want the output of the given User assign to my UserData Model for easy use. i really dont know how to achiev this.
These are my two Files:
User.swift
import SwiftUI
import Firebase
import FirebaseFirestore
struct User {
var username : String
var email : String
}
GetData.swift
let docRef = db.collection("users").document(Auth.auth().currentUser?.uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
The Variable dataDescription is givin me a dictonaryArray, with all the needed values. And now i need to assign that Dictonary to my User.swift struct.
best regards!
fYI: Firebase Printscreen
I'd recommend using Codable where you can. I'm using the CodableFirebase Cocoapod to help with Firebase parsing. Below is an example how to use this:
let docRef = db.collection("users").document(Auth.auth().currentUser?.uid)
docRef.getDocument { (document, error) in
guard let value = document.value, value as? NSNull == nil else {
return
}
do {
let newValue = try self.decoder.decode(User.self, from: value)
///New value created here
} catch let error {
print(error)
}
}
You can make this more generic by passing through a class type and having an optional instance returned:
static func item<T: Codable>(_ item: T.Type, docRef: DatabaseReference, completion: #escaping ((Result<T?, Error>)->Void)) {
docRef.getDocument { (document, error) in
guard let value = snapshot.value, value as? NSNull == nil else {
completion(.success(nil))
return
}
do {
let newValue = try self.decoder.decode(T.self, from: value)
completion(.success(newValue))
} catch let error {
completion(.failure(error))
}
}
}
try
GetData.swift
var user = User()
let docRef = db.collection("users").document(Auth.auth().currentUser?.uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
user.username = document.data(["username"]) as! String
user.email= document.data(["email"]) as! String
} else {
print("Document does not exist")
}
}

Not Able to append the documents retrieved

I am not able to append the documents retrieved from the Firestore database in chat application based on Swift IOS to the "messages" variable, after appending I have configure the table cells as below in the code, I am getting the following error
Error
Cannot convert value of type '[QueryDocumentSnapshot]' to expected argument type 'DocumentSnapshot'
Code
var messages: [DocumentSnapshot]! = []
func configuredatabase ()
{
db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
//here is the error
self.messages.append(documents)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue cell
let cell = self.clientTable .dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
// Unpack message from Firebase DataSnapshot
let messageSnapshot = self.messages![indexPath.row]
guard let message = messageSnapshot as? [String:String] else { return cell }
let name = message[Constants.MessageFields.name] ?? ""
if let imageURL = message[Constants.MessageFields.imageURL] {
if imageURL.hasPrefix("gs://") {
Storage.storage().reference(forURL: imageURL).getData(maxSize: INT64_MAX) {(data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
DispatchQueue.main.async {
cell.imageView?.image = UIImage.init(data: data!)
cell.setNeedsLayout()
}
}
} else if let URL = URL(string: imageURL), let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage.init(data: data)
}
cell.textLabel?.text = "sent by: \(name)"
} else {
let text = message[Constants.MessageFields.text] ?? ""
cell.textLabel?.text = name + ": " + text
cell.imageView?.image = UIImage(named: "ic_account_circle")
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage(data: data)
}
}
return cell
}
While there are two other very good answers, there may be some confusion between a
FIRDocumentSnapshot (Note: renamed to DocumentSnapshot)
Which is returned when you want to get a specific document: someDoc.getDocument(
and
FIRQuerySnapshot (Note: renamed to QuerySnapshot)
Which is returned when an observer is added to a collection or a series of documents is being retrieved: someCollection.getDocuments and then each document within QuerySnapshot is a discreet FIRQueryDocumentSnapshot (renamed to QueryDocumentSnapshot). (e.g. iterate over the QuerySnapshot to get the child QueryDocumentSnapshot)
Note that DocumentSnapshot may return nil in data property if the document doesn't exists, so it can be tested for .exists. Whereas QueryDocumentSnapshot will never be nil (exists is always true) because deleted data is not returned.
In the question, an observer is being added to a collection with
.collection("messages").addSnapshotListener
therefore the data returned is a QuerySnapshot and to store it as a var, the var type would need to match
var messagesQuerySnapshot: QuerySnapshot!
and then inside the listener
db.collection("messages")...addSnapshotListener { querySnapshot, error in
messagesQuerySnapshot = querySnapshot
However, I would not recommend that.
I would suggest a messages class that can be initialize with the data retrieved from Firestore and store those in an array.
class MessagesClass {
var msg_id = ""
var msg = ""
var from = ""
convenience init(withQueryDocSnapshot: QueryDocumentSnapshot) {
//init vars from the document snapshot
}
}
and then a class var datasource array to hold them
var messagesArray = [MessageClass]()
and then code to read the messages, create the message objects and add them to the dataSource array
db.collection("messages")...getDocuments { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
for doc in snapshot.documents {
let aMsg = MessageClass(withQueryDocSnapshot: doc)
self.messagesArray.append(aMsg)
}
}
NOTE: we are not adding an listener here, we are getting the documents one time. If you want to add a listener to watch for users being added, changed or removed, additional code is needed to detect the changes.
See the Firebase Documentation on Viewing Changes Between Snapshots
Replace self.messages.append(documents) with self.messages.append(contentsOf: documents)
The first method takes a single element and the second one takes a collection which is in your case.
https://developer.apple.com/documentation/swift/array/3126937-append
https://developer.apple.com/documentation/swift/array/3126939-append
var messages: [[String: Any]] = []
db.collection("messages").document("hello").collection("newmessages").document("2").collection("hellos").document("K").collection("messages").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
for doc in documents {
self.messages.append(doc.data())
}
}

How to get data from a map (object) Cloud Firestore document on Swift 4.2?

I am using Cloud Firestore as a Database for my iOS App.
I have an issue while I want to query my data when the document contains Maps (Object Type). I can not access the fields (say: firstName here) of a map (nameOnId) by having the simple query.
My code is as bellow:
let db = Firestore.firestore()
db.collection("userDetails").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let results = document.data()
let result2 = results.compactMap({$0})
print("listedItems: \(document.documentID) => \(result2[0].value)") }}}
I read somewhere that in order to be able to access the values inside the map object, I need to flatten the map object, but having that does not give me access to them, the only thing that I could get into are a group of values inside the map so it only shows the keys and values for them like:
{
firstName = "John";
middleName = "British";
middleName = "Citizen";
gender = "M";
DOB = "8 December 2000 at 00:00:00 UTC+11";
}
the question is how to get access to a single value like "John" using the query?My data structure on Cloud Firestore
One way of doing it is as follows. Also its good practice to not force unwrap your querySnapshot (if the query does not exist, your app will crash!). Hope this helps!
let db = Firestore.firestore()
db.collection("userDetails").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else if let querySnapshot = querySnapshot {
for document in querySnapshot.documents {
let results = document.data()
if let idData = results["nameOnID"] as? [String: Any] {
let firstName = idData["firstName"] as? String ?? ""
print(firstName)
}
}
}
}

How to get post count of user using firestore in iOS swift

I have migrated user post, followers and following from from firebase to firestore. Now i have migrated post, followers and following and post, followers count too.
Here the code i have migrated from firebase to firestore.
import Foundation
import FirebaseDatabase
import FirebaseFirestore
class FollowApi {
var REF_FOLLOWERS = Database.database().reference().child("followers")
var REF_FOLLOWING = Database.database().reference().child("following")
let db = Firestore.firestore()
func followAction(withUser id: String) {
let docRef = db.collection("user-posts").document(id)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
self.db.collection("feed").document(API.User.CURRENT_USER!.uid).setData([document.documentID: true])
} else {
print("Document does not exist")
}
}
self.db.collection("followers").document(id).setData([API.User.CURRENT_USER!.uid: true])
self.db.collection("following").document(API.User.CURRENT_USER!.uid).updateData([id: true])
}
func unFollowAction(withUser id: String) {
let docRef = db.collection("user-posts").document(id)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
self.db.collection("feed").document(API.User.CURRENT_USER!.uid).delete()
} else {
print("Document does not exist")
}
}
self.db.collection("followers").document(id).setData([API.User.CURRENT_USER!.uid: NSNull()])
self.db.collection("following").document(API.User.CURRENT_USER!.uid).setData([id: NSNull()])
}
func isFollowing(userId: String, completed: #escaping (Bool) -> Void) {
let docRef = db.collection("followers").document(userId)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
print("documnetData::::\(String(describing: document.data()))")
if let dataDescription = document.data(), let _ = dataDescription[API.User.CURRENT_USER!.uid] as? Bool {
completed(true)
}
completed(false)
} else {
completed(false)
}
}
}
func fetchCountFollowing(userId: String, completion: #escaping (Int) -> Void) {
// REF_FOLLOWING.child(userId).observe(.value, with: {
// snapshot in
// let count = Int(snapshot.childrenCount)
// completion(count)
// })
db.collection("following").document(userId).getDocument { (querySnapshot, error) in
let count = Int((querySnapshot?.documentID)!)
print("followingCount::::\(String(describing: count))")
completion(count!)
}
}
}//followAPI
I tried to get following counts from firestore.
let count = Int((querySnapshot?.documentID)!)
print("followingCount::::\(String(describing: count))")
completion(count!)
but does not show any any yet all. I do not know what mistake i have done ?
Any help much appreciated pls....
If you're querying for a collection then its snapshot will contain an array of documents. What are you trying to get is a documentID which is same as key in Firebase.
Firestore | Firebase
documentID = snapshot.key
documentData = snapshot.value
Now, Come to the main point and here is what you need to get the count.
let count = querySnapshot?.documents.count
print(count)
EDIT For Comment: how can i migrate REF_FOLLOWING.child(userId).observe(.value, with: { snapshot in let count = Int(snapshot.childrenCount) completion(count) }) to firestore
Based on attached DB structure you're fetching following corresponding to userId which is a Document.
REF_FOLLOWING.document(userId).getDocument { (snapshot, error) in
if let _error = error {
print(_error.localizedDescription)
return
}
guard let _snapshot = snapshot else {return}
/// This is a single document and it will give you "[String:Any]" dictionary object. So simply getting its count is the result you needed.
let dict = _snapshot.data()
print(dict.count)
}

Resources