Swift Firestore custom objects from query while listening for realtime updates with snapshotListener - swift

I've got issues converting querySnapshots that is observed for changes with snapshotListener to custom objects.
This is my code but the compiler is complaining:
func getCartData(db: Firestore){
db.collection("test")
.whereField("Cart", arrayContains: "testItem")
.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching document: \(error!)")
return
}
for doc in documents {
guard let data = doc.data() else { //here is an error saying: Initializer for conditional binding must have Optional type, not '[String : Any]'
print("Document data was empty.")
return
}
let result = Result {
try doc.data(as: CartDataDocument.self)
}
switch result {
case .success(let userDataDoc):
if let userDataDoc = userDataDoc {
// A `userDataDoc` value was successfully initialized from the DocumentSnapshot.
self.cartData = userDataDoc
} 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 `userDataDoc` value could not be initialized from the DocumentSnapshot.
print("Error decoding UserDataDocument: \(error)")
}
}
}
}
I was able to make custom objects of one Firestore document that got listened to with a snapshotListener but had no luck with this query.

The error seems correct. If you look at the declaration that Xcode shows while autocompleting, doc.data() is indeed a non-nullable [String: Any].
If you want to check if a document is empty you can do it like this:
guard !doc.data().isEmpty else {
print("Document data was empty.")
return
}
Otherwise, you could also override init(from decoder: Decoder) throws in CartDataDocument and provide default values for all keys. This obviously goes in a a different direction: instead of ignoring empty documents, you are adding them to the list with default values. But I've got to ask: why do you have empty documents?

Related

addSnapshotListener code compiles and works but getting error: Cannot convert value of type '()' to closure result type 'Title?'

The code compiles and works but throwing the error when viewing the code.
func subscribe() async {
let uid = auth.currentUser.uid
print("Library.subscribe() \(uid)")
if listenerRegistration == nil {
listenerRegistration = db.collection(collectionRootLib).document(uid).collection(collectionUserLib).addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.content = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Title.self) // <= cannot convert Error
}
}
}
}
The two errors thrown are:
Argument passed to call that takes no arguments
Cannot convert value of type '()' to closure result type 'Title?'
Putting the try into a do/catch block doesn't seem to be the correct fix.
Updated to Xcode-13.4.1 and no longer have these compiler errors showing when viewing Firebase codable extensions.

Array populated from Firebase returned empty, but data was retrieved. Why?

Still learning some swift and managed to advance and retrieving data from a firestore database. I have a Data Controller whose task is to offload all the data retrieving from firestore. It does the calls and gets data, but when returning the info from the first function I have implemented on it, it's empty.
Here's an example of the funcion:
func fetchUnidades(for foo: MyFirstEnum, and bar: MySecondEnum ) -> [MyClassType]{
let db = Firestore.firestore()
let colPath = "my/firebase/path"
let results = [MyClassType]()
let collection = db.collection(colPath)
collection.whereField("myField", isEqualTo: foo.rawValue).getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
do {
print("\(document.documentID) => \(document.data())") // 1st print
let newMyClass = try document.data(as: MyClassType.self)
results.append(newMyClass)
print("here") // 2nd print - debug breakpoint
}catch (let error) {
print("\(error)")
}
}
}
}
print("DC - Recovered \(results.count) results")
return results
}
Assume MyFirstEnum, MySecondEnum and MyClassType are correct, because the database retrieves info. On the 1st print line, there's output for data retrieved, and on the 2nd print - debug breakpoint line, if I do a po results, it has one value, which is the one retrieved as you can see here:
unidades being the name on my code of results on this example.
But right after continuing with the execution, unidades, aka results is empty, the line:
print("DC - Recovered \(results.count) results")
prints DC - Recovered 0 results and the return also returns an empty array with zero values on it.
Any idea about why this might be happening? And how to solve the issue? Obviously the goal is to return the info...
That's because the result comes asynchronously. Your fetchUnidades returns results array before it's populated.
You need to add a completion closure in this case. Instead of returning results you call that completion closure and pass the results as its argument.
func fetchUnidades(for foo: MyFirstEnum, and bar: MySecondEnum, completion: (results: [MyClassType]?, error: Error?) -> Void) {
let db = Firestore.firestore()
let colPath = "my/firebase/path"
let collection = db.collection(colPath)
collection.whereField("myField", isEqualTo: foo.rawValue).getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
completion(nil, err)
} else {
let results = [MyClassType]()
for document in querySnapshot!.documents {
do {
print("\(document.documentID) => \(document.data())") // 1st print
let newMyClass = try document.data(as: MyClassType.self)
results.append(newMyClass)
print("here") // 2nd print - debug breakpoint
}catch (let error) {
print("\(error)")
}
}
completion(results, nil)
}
}

Unable to Fetch Users Firebase Firestore

Unable to fetch list of users from firebase. print returns as [] . Code is below. Any help would be awesome!
let COLLECTION_USERS = Firestore.firestore().collection("users")
func fetchUsers() {
COLLECTION_USERS.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else {return}
self.users = documents.compactMap({try? $0.data(as: User.self)})
print(self.users)
}
I expect the 7 users that I have signed up to print that data.
If documents.count shows that you've got 7 documents, but self.users contains no elements after running the mapping, this indicates your Firestore documents can't be mapped to your Swift structs.
Please make sure that the data types on your Swift structs match the types used in your Firestore documents.
You should also use code that is more error-resilient. In your code, you explicitly drop the error parameter on the closure - you rather don't want to do this.
The following code snippet (taken from the official docs) shows how to do this.
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)")
}
}

How can I add these Firestore fields to a Dictionary?

I am looking to add all my "usernames" into a dictionary. I am having some trouble doing this. I am sure it's very obvious, but I am very new to coding.
I am stuck at, right now and can't seem to find a clear answer anywhere:
func fetchUser() {
let db = Firestore.firestore()
let usernameSearch = db.collection("users")
usernameSearch.getDocuments { (snapshot, error) in
if error != nil {
print("Error obtaining usernames")
} else {
for field in snapshot!.documents {
let field = field.get("username")
print(field!)
}
}
}
}
I would really appreciate it if somebody could help me out. I am sure it's very obvious, or I'm just doing it totally wrong.
First, get into the habit of safely unwrapping over force unwrapping. And choose more accurate names for your objects (i.e. usersCollection over usernameSearch). However, in this case, there's no need to instantiate individual properties for the database and the collection since they're not being used anywhere else but here (so be efficient and omit them).
var usersDictionary = [String: [String]]()
func fetchUser() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot { // unwrap the snapshot safely
var usernames = [String]()
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
usernames.append(username)
}
}
usersDictionary["usernames"] = usernames
} else {
if let error = error {
print(error)
}
}
}
}
Or if you actually meant an array of users:
var usersArray = [String]()
func fetchUser() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot { // don't force unwrap with !
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
usersArray.append(username)
}
}
} else {
if let error = error {
print(error)
}
}
}
}
I'm assuming that what you're looking for is an Array, not a Dictionary. I'll also assume that you are indeed getting the correct value that you'd expect out of field.get("username"), e.g. a string such as "Bob." Therefore, what you are trying to do is map the list of document objects to a list of strings.
If you scroll to the Topics section of the Array documentation from Apple, you can find some of the operations they provide for arrays such as snapshot!.documents.
One of those operations is actually map, and its description is:
Returns an array containing the results of mapping the given closure over the sequence’s elements.
https://developer.apple.com/documentation/swift/array/3017522-map
In other words, you provide a transformation to perform for each instance of a document belonging to the snapshot!.documents Array and get back a new Array containing the resultant values of that transformation.
In this case I will use a more specific operation; compactMap. We have to try and cast the returned value from Any to String. If that does not succeed, it will return nil, and we'll want to filter that out. I expect it to be an unlikely case due to the type requirements made by the Firebase Console, but it's good to be aware of it. Here is the example:
func fetchUsernames(from usernameCollection: String, completion: #escaping ([String]) -> Void) {
let db = Firestore.firestore()
let collection = db.collection(usernameCollection)
collection.getDocuments { snapshot, error in
guard error != nil,
let usernames = snapshot?.documents.compactMap { $0.get("username") as? String }
else { return print("Error obtaining usernames") }
completion(usernames)
}
}
The key line here being let usernames = snapshot?.documents.compactMap { $0.get("username") }. We are passing the map function a closure. This closure is passed an argument itself; each value from the snapshot?.documents array. You may refer to this passed in value with $0.

Firestore Query Swift 4.0 Missing Return

I have the following query;
fileprivate func observeQuery() {
guard let query = query else { return }
stopObserving()
listener = query.addSnapshotListener({ [unowned self] (snapshot, error) in
guard let snapshot = snapshot else { return }
let models = snapshot.documents.map({ (document) -> Post in
if let model = Post(dictionary: document.data()) {
return model
} else {
print(error as Any)
}
}) //here
self.posts = models
self.documents = snapshot.documents
})
}
I am getting "Missing return in a closure expected to return 'Post'" mentioned as "//here" in the code. I have return model which is of type Post and I cannot access model after the closure. I have used the GitHub files here;
Firestore GitHub iOS Quickstart
This error doesn't make sense to me can someone please shed some light on the matter?
Many thanks as always.
Your issue is that not all code branches return inside the closure of your map statement. You should change map to flatMap, this way you can also get rid of the if statement by simply returning the failable initializer's result inside your closure, since flatMap will filter out all nil return values.
let models = snapshot.documents.flatMap({ document in Post(dictionary: document.data())})