let db = Firestore.firestore()
let docRef = db.collection("users").document(result!.user.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)")
print()
} else {
print("Document does not exist")
}
}
print("Document data: \(dataDescription)") outputs the following:
Document data: ["uid": LjqBXo41qMStt89ysQ4I9hxla2h1, "firstname": Tim, "lastname": Dorsemagen]
How can I extract each value such as the uid, the firstname and the lastname from dataDescription?
There are several ways to accomplish this. One of the most easily understood would be to break the string down in sections. Assuming you only want the values rather than their keys:
let values = dataDescription
.dropFirst()
.dropLast()
.components(separatedBy: ",")
.map{$0.components(separatedBy: ":")}
.map{$0.last!.trimmingCharacters(in: .whitespaces)}
print(values) //["LjqBXo41qMStt89ysQ4I9hxla2h1", "Tim", "Dorsemagen"]
Firestore has everything needed to easily get components of a document. Here's an asynchronous example of reading a users name from a document and returning it
func getUserAsync() async -> String{
let usersCollection = self.db.collection("users") //self.db is my Firestore
let thisUserDoc = usersCollection.document("uid_0")
let document = try! await thisUserDoc.getDocument()
let name = document.get("name") as? String ?? "No Name"
return name
}
if you want to use Codable (advised! See Mapping Firestore Data), this works for printing the name (can also be combined with the above solution)
func readUser() {
let usersCollection = self.db.collection("users") //self.db is my Firestore
let thisUserDoc = usersCollection.document("uid_0")
thisUserDoc.getDocument(completion: { document, error in
if let doc = document {
let user = try! doc.data(as: UserCodable.self)
print(user.name) //assume the UserCodable object has a name property
}
}
}
or just a regular old read of a document and print the name
func readUser() {
let usersCollection = self.db.collection("users") //self.db is my Firestore
let thisUserDoc = usersCollection.document("uid_0")
thisUserDoc.getDocument(completion: { document, error in
let name = document?.get("name") as? String ?? "No Name"
print(name)
})
}
*note: no error checking and I am force unwrapping options. Don't do that.
Related
Is there a way to map a sub-collection into a struct while querying the main collection?
My sub-collection structure is:
-conversations (collection)
--- "users" : [array of two users]
--- messages (subcollection)
------- "created by": "sender"
------- "date" : timestamp
------- "msg" : "help me guys I'm stuck with this damn chat feature"
I read somewhere the query are shallow meaning you'll only get the fields of the collecting you're querying, but maybe there is another way besides nested query loops after querying the first collection
func getFilteredConversations(query: String) {
if (user != nil) {
db.collection("conversations").whereField("users", arrayContains: user!.displayName)
.order(by: "createdTime")
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no conversations found")
return
}
//mapping
self.chats = documents.map {(queryDocumentSnapshot) -> Conversation in
let data = queryDocumentSnapshot.data()
let docId = queryDocumentSnapshot.documentID
let users = data["users"] as? [String] ?? [""]
let unreadmsg = data["hasUnreadMessage"] as? Bool ?? false
//MARK: - GET MESSAGES
self.db.collection("conversations").document(docId).collection("messages")
.order(by: "date")
.addSnapshotListener{ (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {
print("no messages found")
return
}
var mensajes = [Message]()
mensajes = documents.map {(queryDocumentSnapshot) -> Message in
let data = queryDocumentSnapshot.data()
let docId = queryDocumentSnapshot.documentID
let createdby = data["created_by"] as? String ?? ""
let msg = data["msg"] as? String ?? ""
let date = data["date"] as? Timestamp ?? Timestamp()
return Message(createdBy: createdby, msg: msg, date: date, id: docId)
}
}
print("Users: \(users)")
return Conversation(id: docId, users: users, messages: mensajes, hasUnreadMessage: unreadmsg)
}
}
}
}
this is the model
struct Conversation: Decodable, Identifiable {
//var id: UUID { person.id }
#DocumentID var id: String? = UUID().uuidString
var users: [String] = [""]
var messages: [Message] = []
var hasUnreadMessage : Bool = false
}
struct Message: Decodable {
var createdBy: String?
var msg: String?
#ServerTimestamp var date : Timestamp?
var id : String?
}
You've already found the answer yourself it seems: Firestore queries are shallow. There is no way to read from the subcollection while reading the parent document. The only way to query across collections is with a collection group query, which doesn't seem to apply here.
Consider if it's worth duplicating the most recent messages from the conversation in the parent document, either through the client-side code or in a Cloud Function. That way you can reduce the number of documents you have to read to get the initial messages to display to the user.
Below is a section of a query I am attempting to accomplish in Swift.
What I can't grasp is why :
room = Room(images: images, name: name)
rooms.append(room)
runs before:
imageQuery.getDocuments()
{
(QuerySnapshot, err) in
if let err = err{}
else
{
for document in QuerySnapshot!.documents
{
let imageURL = document.data()["imageURL"] as? String ?? ""
let image = ImageModel(imageURL: imageURL, date: Date(timeIntervalSinceNow: 0))
images.append(image)
}
The second snippet of code does run, but it runs after the first snippet. But I need the values from the second one to populate the values in the first. I'm not new to programming but I am new to swift and I cannot seem to figure out how to make the outer loop go into the inner loop before running the rest of its' code. Thank you for your help!
Here is the majority of the function:
let roomQuery = projectRef.document(document.documentID).collection("Rooms")
roomQuery.getDocuments()
{
(QuerySnapshot, err) in
if let err = err{}
else
{
var room:Room
for document in QuerySnapshot!.documents
{
let name = document.data()["name"] as? String ?? ""
var images = [ImageModel]()
let imageQuery = roomQuery.document(document.documentID).collection("Images")
imageQuery.getDocuments()
{
(QuerySnapshot, err) in
if let err = err{}
else
{
for document in QuerySnapshot!.documents
{
let imageURL = document.data()["imageURL"] as? String ?? ""
let image = ImageModel(imageURL: imageURL, date: Date(timeIntervalSinceNow: 0))
images.append(image)
}
}
}
room = Room(images: images, name: name)
rooms.append(room)
}
}
because you're using a closure, not a loop, so it is executed asynchronoulsy
Screenshot of a Firestore Document
I am using Swift, Xcode and a Firestore database.
I created a TableView and a Custom Object Class (MediumSample) with a dictionary and want to load my Firestore documents and show them in the TableView.
The documents (example in the screenshot) are loaded from Firestore correctly but the conversion into the object did not work. The list of objects returned from compactMap is always empty.
Here is my code snippets. It would be great, if someone has a hint on what is going wrong.
Custom Object Class (simplified):
import Foundation
import FirebaseFirestore
protocol MediumSampleDocumentSerializable {
init?(dictionary: [String:Any])
}
struct MediumSample {
var id: String
var field_t: String
var field_i: Int64
var field_b1: Bool
var field_b2: Bool
var field_g: FirebaseFirestore.GeoPoint
var field_d: Date
var field_a: [String]
var usecase: String
var dictionary: [String:Any] {
return [
"id": id,
"field_t": field_t,
"field_i": field_i,
"field_b1": field_b1,
"field_b2": field_b2,
"field_g": field_g,
"field_d": field_d,
"field_a": field_a,
"usecase": usecase
]
}
}
extension MediumSample: MediumSampleDocumentSerializable {
init?(dictionary: [String:Any]) {
guard let id = dictionary ["id"] as? String,
let field_t = dictionary ["field_t"] as? String,
let field_i = dictionary ["field_i"] as? Int64,
let field_b1 = dictionary ["field_b1"] as? Bool,
let field_b2 = dictionary ["field_b2"] as? Bool,
let field_g = dictionary ["field_g"] as? FirebaseFirestore.GeoPoint,
let field_d = dictionary ["field_d"] as? Date,
let field_a = dictionary ["field_a"] as? [String],
let usecase = dictionary ["usecase"] as? String else {return nil}
self.init (id: id, field_t: field_t, field_i: field_i, field_b1: field_b1, field_b2: field_b2, field_g: field_g, field_d: field_d, field_a: field_a, usecase: usecase)
}
}
Declaration of the database and array and calling the loading function:
import UIKit
import FirebaseFirestore
class MediumTableViewController: UITableViewController {
//MARK: Properties
var db: Firestore!
var mediumsamples = [MediumSample]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
loadMediumSamples()
}
Function for loading the Firestore documents to fill the Array:
private func loadMediumSamples() {
//run the Firestore query
db.collection(Constants.MEDIUMS).whereField("usecase", isEqualTo: Constants.USECASE)
.getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
//initialise an array of medium objects with Firestore data snapshots
self.mediumsamples = querySnapshot!.documents.compactMap({MediumSample(dictionary: $0.data())})
//fill the tableView
DispatchQueue.main.async {
self.tableView.reloadData()
print(self.mediumsamples)
}
print("Mediums List", self.mediumsamples) // This line returns: Mediums List []
print("Mediums List size", (self.mediumsamples.count)) // This line returns: Mediums List size 0
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())") // This line returns the snapshot documents correctly!
}
}
}
}
Here is how the screenshot object object is added:
func addMediumSamples() {
let currentDateTime = Date()
let location = FirebaseFirestore.GeoPoint(latitude: 0, longitude: 0)
let mediumsample = MediumSample(id: "an id", field_t: "field_t", field_i: 10, field_b1: true, field_b2: false, field_g: location, field_d: currentDateTime, field_a: [Constants.SAMPLE_DEV], usecase: Constants.SAMPLE_DEV)
var ref: DocumentReference? = nil
ref = self.db.collection(Constants.MEDIUMS).addDocument(data: mediumsample.dictionary) {
error in
if let error = error {
print("Error writing city to Firestore: \(error)")
} else {
print("Document added with id : \(ref!.documentID)")
}
}
}
The problem is in the MediumSample struct, in the field_d type (Date).
The type of that field in your Cloud Firestore database is Timestamp.
The field "field_d" in the MediumSample struct expects a value of type Date.
You can change the type to the FirebaseFirestore.Timestamp, or you can convert it to Date when mapping and before passing to the MediumSample.
eg. for converting Timestamp to Date in Swift
let date = timestamp.dateValue()
I'm trying to get the old value of a field when it is changed in Firestore. Is there any way to access what the previous value of a field is after it is changed? Here is my current code, I want to access the old nickName under .modified and print out what the new nickName is and also the old one.
db.collection("cities").whereField("state", isEqualTo: "CA").addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
print("New city: \(diff.document.data())")
let nickName = myData["nickName"] as? String ?? ""
}
if (diff.type == .modified) {
let nickName = myData["nickName"] as? String ?? ""
}
if (diff.type == .removed) {
let nickName = myData["nickName"] as? String ?? ""
}
}
}
Unfortunately, that is not a feature of Firestore. What you can do is have another field oldNickName and using Firebase Functions, automatically update that when the nickName field is changed.
The best solution is storing nickName locally, so you can refer back to your local variable when nickName changes, accessing the newly updated one in the database and the previous nickName locally. Here is the updated code:
var nickNames = [String : String]()
db.collection("cities").whereField("state", isEqualTo: "CA").addSnapshotListener { snapshot, error in
guard error == nil, let snapshot = snapshot?.documentChanges else { return }
snapshot.forEach {
let document = $0.document
let documentID = document.documentID
let nickName = document.get("nickName") as? String ?? "Error"
switch $0.type {
case .added:
print("New city: \(document.data())")
nickNames[documentID] = nickName
case .modified:
print("Nickname changed from \(nickNames[documentID]) to \(nickName)")
nickNames[documentID] = nickName
case .removed:
print("City removed with nickname \(nickNames[documentID])")
nickNames.removeValue(forKey: documentID)
}
}
}
nickNames is a dictionary with key cityID and value nickName. This code is written in Swift 5.
I want to be able to list users in a tableview by fetching their UID Value from a key stored in my database. As of now, the code only fetches the first value in the database rather than all of the values. Here is the code for fetching the applicants.
func loadApplicants() {
let usersRef = ref.child("users")
usersRef.observe(.value, with: { (users) in
var resultArray = [UserClass]()
for user in users.children {
let user = UserClass(snapshot: user as! DataSnapshot)
if user.uid == self.job.userID {
let appRef = self.ref.child("jobs").child(self.job.postID).child("applicants")
appRef.queryOrderedByKey().observeSingleEvent(of: .childAdded, with: { (snapshot) in
let sValue = snapshot.value
resultArray.append(user)
})
}
}
}) { (error) in
print(error.localizedDescription)
}
}
This is what my database looks like where the User's UIDs are stored.
jobs
"Job ID"
applicants:
-KtLJaQnFMnyI-MDWpys:"8R6ZAojX0FNO7aSd2mm5aQXQFpk1"
-KtLLBFU_aVS_xfSpw1k:"GGqjtYvwSwQw9hQCVpF4lHN0kMI3"
If I was to run the app, it fetches UID: "8R6ZAojX0FNO7aSd2mm5aQXQFpk1"
How can I implement a for loop or an if statement to ensure that all of the values are taken and appended into the table view
I know that I need a for loop before the fetchApplicants is called from AuthService because it is only fetching one UID but I can't work out where it would go.
Thanks.
P.S. This is what I have tried
func loadApplicants() {
let jobID = job.postID
let appRef = ref.child("jobs").child(jobID!).child("applicants")
appRef.queryOrderedByKey().observeSingleEvent(of: .childAdded, with: { (snapshot) in
if let applicants = snapshot.value! as? [String:AnyObject] {
for (value) in applicants {
self.authService.fetchApplicants(applicantID: "\(value!)", completion: { (users) in
self.usersArray = users
self.tableView.reloadData()
})
}
}
})
}
but the output is:
(key: "-KtLLBFU_aVS_xfSpw1k", value: GGqjtYvwSwQw9hQCVpF4lHN0kMI3)
(key: "-KtLJaQnFMnyI-MDWpys", value: 8R6ZAojX0FNO7aSd2mm5aQXQFpk1)
Needed to use observe instead of observeSingleEvent
Answer:
appRef.queryOrderedByKey().observe(.childAdded, with: { (snapshot) in