Why I couldn't assign fetched values from Firestore to an array in Swift? - ios

I tried several times with various ways to assign the collected values of documents from firestore into an array. Unfortunately, I could't find a way to solve this issue. I attached the code that I recently tried to implement. It includes before Firestore closure a print statement which print the whole fetched values successfully. However, after the closure and I tried to print the same array and the result is an empty array.
I tried to implement this code
var hotelCities: [String] = []
func getCities() {
db.collection("Hotels").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var found = false
let documentDetails = document.data() as NSDictionary
let location = documentDetails["Location"] as! NSDictionary
let city = location["city"]!
if (self.hotelCities.count == 0) {
self.hotelCities.append(String(describing: city))
}
else{
for item in self.hotelCities {
if item == String(describing: city){
found = true
}
}
if (found == false){
self.hotelCities.append(String(describing: city))
}
}
}
}
print(self.hotelCities)
}
print(self.hotelCities)
}

That's actually the expected result, since data is loaded from Firestore asynchronously.
Once you call getDocuments(), the Firestore client goes of and connects to the server to read those documents. Since that may take quite some time, it allows your application to continue running in the meantime. Then when the documents are available, it calls your closure. But that means the documents are only available after the closure has been called.
It's easiest to understand this flow, by placing a few print statements:
print("Before starting to get documents");
db.collection("Hotels").getDocuments() { (querySnapshot, err) in
print("Got documents");
}
print("After starting to get documents");
When you run this code, it will print:
Before starting to get documents
After starting to get documents
Got documents
Now when you first saw this code, that is probably not the output your expected. But it completely explains why the print(self.hotelCities) you have after the closure doesn't print anything: the data hasn't been loaded yet.
The quick solution is to make sure that all code that needs the documents is inside of the close that is called when the documents are loaded. Just like your top print(self.hotelCities) statement already is.
An alternative is to define your own closure as shown in this answer: https://stackoverflow.com/a/38364861

Related

Listener event not triggered when document is updated (Google Firestore)

I am struggling to understand why my event listener that I initialize on a document is not being triggered whenever I update the document within the app in a different UIViewController. If I update it manually in Google firebase console, the listener event gets triggered successfully. I am 100% updating the correct document too because I see it get updated when I update it in the app. What I am trying to accomplish is have a running listener on the current user that is logged in and all of their fields so i can just use 1 global singleton variable throughout my app and it will always be up to date with their most current fields (name, last name, profile pic, bio, etc.). One thing I noticed is when i use setData instead of updateData, the listener event gets triggered. For some reason it doesn't with updateData. But i don't want to use setData because it will wipe all the other fields as if it is a new doc. Is there something else I should be doing?
Below is the code that initializes the Listener at the very beginning of the app after the user logs in.
static func InitalizeWhistleListener() {
let currentUser = Auth.auth().currentUser?.uid
let userDocRef = Firestore.firestore().collection("users").document(currentUser!)
WhistleListener.shared.listener = userDocRef.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("INSIDE LISTENER")
}
}
Below is the code that update's this same document in a different view controller whenever the user updates their profile pic
func uploadProfilePicture(_ image: UIImage) {
guard let uid = currentUser!.UID else { return }
let filePath = "user/\(uid).jpg"
let storageRef = Storage.storage().reference().child(filePath)
guard let imageData = image.jpegData(compressionQuality: 0.75) else { return }
storageRef.putData(imageData) { metadata, error in
if error == nil && metadata != nil {
self.userProfileDoc!.updateData([
"profilePicURL": filePath
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
}
}
You can use set data with merge true it doesn't wipe any other property only merge to specific one that you declared as like I am only update the name of the user without wiping the age or address
db.collection("User")
.document(id)
.setData(["name":"Zeeshan"],merge: true)
The answer is pretty obvious (and sad at the same time). I was constantly updating the filepath to be the user's UID therefore, it would always be the same and the snapshot wouldn't recognize a difference in the update. It had been some time since I had looked at this code so i forgot this is what it was doing. I was looking past this and simply thinking an update (no matter if it was different from the last or not) would trigger an event. That is not the case! So what I did was append an additional UUID to the user's UID so that it changed.

Unexpected behavior of FireStore listener

I am working on an iOS app using Firebase as backend. I am encountering a problem where a listener on a sub collection is behaving unexpectedly. Let me explain my data models first:
I have a top-level collection called "families". Within this collection, I have a sub-collection called "chores". It looks something like this:
Within my iOS app, I am adding a listener to this "chores" sub collection like this:
func readChoreCollection(_ familyId: String) {
if familyChoresListener == nil {
let choreCollection = database.collection("families").document(familyId).collection("chores")
familyChoresListener = choreCollection.order(by: "created")
.addSnapshotListener(includeMetadataChanges: false) { [weak self] querySnapshot, error in
print("\(#fileID) \(#function): \(choreCollection.path)")
guard let querySnapshot = querySnapshot else {
print("\(#fileID) \(#function): Error fetching documents: \(error!)")
return
}
let chores: [Chore] = querySnapshot.documents
.compactMap { document in
do {
return try document.data(as: Chore.self)
} catch {
print("\(#fileID) \(#function): error")
return nil
}
}
if chores.isEmpty {
print("\(#fileID) \(#function): received empty list, publishing nil...")
self?.familyChoresPublisher.send(nil)
} else {
print("\(#fileID) \(#function): received chores data, publishing ... \(querySnapshot.metadata.hasPendingWrites)")
self?.familyChoresPublisher.send(chores)
}
}
}
}
According to the Firestore doc:
The snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified
So, when I add a new document to the "chores" sub-collection, the listener did trigger, that is expected. However, it is triggered twice, one from local change, and one from remote change. As shown in the log below:
ChoreReward/ChoreRepository.swift readChoreCollection(_:): received chores data, publishing ... true
ChoreReward/ChoreService.swift addSubscription(): received and cached a non-nil chore list
ChoreReward/ChoreRepository.swift readChoreCollection(_:): families/tgO0B4bjq8uwAzmBaOtL/chores
ChoreReward/ChoreRepository.swift readChoreCollection(_:): received chores data, publishing ... false
ChoreReward/ChoreService.swift addSubscription(): received and cached a non-nil chore list
You can see that the listener is called twice, one with hasPendingWrites = true and one with hasPendingWrites = false. So the documentation did mentioned that the local changes will fire-off the callback to listener first before sending data back to Firestore. So this behavior is kinda expected??? On my other listeners (document listeners) within the app, they are only getting called once by the remote changes, not twice. Maybe there is a different in behavior of document vs. collection/query listener? Can anybody verify this difference?

I can't get array and put in the another array swift from Firebase storage

I want get folders and images from Firebase storage. On this code work all except one moment. I cant append array self.collectionImages in array self.collectionImagesArray. I don't have error but array self.collectionImagesArray is empty
class CollectionViewModel: ObservableObject {
#Published var collectionImagesArray: [[String]] = [[]]
#Published var collectionImages = [""]
init() {
var db = Firestore.firestore()
let storageRef = Storage.storage().reference().child("img")
storageRef.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for prefixName in result.prefixes {
let storageLocation = String(describing: prefixName)
let storageRefImg = Storage.storage().reference(forURL: storageLocation)
storageRefImg.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for item in result.items {
// List storage reference
let storageLocation = String(describing: item)
let gsReference = Storage.storage().reference(forURL: storageLocation)
// Fetch the download URL
gsReference.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
// Get the download URL for each item storage location
let img = "\(url?.absoluteString ?? "placeholder")"
self.collectionImages.append(img)
print("\(self.collectionImages)")
}
}
}
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
//
self.collectionImagesArray.append(self.collectionImages)
}
}
}
If i put self.collectionImagesArray.append(self.collectionImages) in closure its works but its not what i want
The problem is caused by the fact that calling downloadURL is an asynchronous operation, since it requires a call to the server. While that call is happening, your main code continues so that the user can continue to use the app. Then when the server returns a value, your closure/completion handler is invoked, which adds the URL to the array. So your print("\(self.collectionImagesArray)") happens before the self.collectionImages.append(img) has ever been called.
You can also see this in the order that the print statements occur in your output. You'll see the full, empty array first, and only then see the print("\(self.collectionImages)") outputs.
The solution for this problem is always the same: you need to make sure you only use the array after all the URLs have been added to it. There are many ways to do this, but a simple one is to check whether your array of URLs is the same length as result.items inside the callback:
...
self.collectionImages.append(img)
if self.collectionImages.count == result.items.count {
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
Also see:
How to wait till download from Firebase Storage is completed before executing a completion swift
Closure returning data before async work is done
Return image from asynchronous call
SwiftUI: View does not update after image changed asynchronous

why I can't get all documents stored in firestore sub collection?

I have users collection that has sub collection called attendedEvents like the picture below. as you can see there are 3 documents in the attendedEvents sub collection
I try to get all documents available on that sub collection by using the code below, I just want to get it all, without order, limit or anything using getDocuments
func getAttendedEventsFromBeginning(completion: #escaping (_ eventID: [String]?,QueryDocumentSnapshot?)->Void) {
FirestoreDocumentReference.users(uidUser: uid).reference().collection("attendedEvents")
.getDocuments { (snapshot, error) in
let lastDocument = snapshot?.documents.last
if let error = error {
completion(nil,lastDocument)
print("Error when fetching attended events documents in user subcollection: \(error.localizedDescription)")
} else {
print("Successfully fetching attended events documents in user subcollection from Firestore ")
guard let documentsSnapshot = snapshot else {
completion(nil,lastDocument)
return
}
let eventDocuments = documentsSnapshot.documents
print("xxxxx")
print(eventDocuments)
var attendedeventIDs = [String]()
for document in eventDocuments {
let eventDictionary = document.data()
let theEvent = eventDictionary["eventID"] as! String
attendedeventIDs.append(theEvent)
}
print(attendedeventIDs)
completion(attendedeventIDs,lastDocument)
}
}
}
but as a result, I just got 2 document snapshot, it should be 3 documents
but if I delete the app from simulator and install it again, I got all the three data. is is cached or what?
I have been in the same situation. In my case, this happens because in my document it only contains subcollection. And that will cause the document itself to not be shown in queries or snapshots.
My walkaround method is to add some random info in the document to make it exist.

Delete duplicated object in core data (swift)

I'm saving objects to core data from a JSON, which I get using a for loop (let's say I called this setup function.
Because the user might stop this loop, the objects saved in core data will be partial. The user can restart this setup function, restarting the parsing and the procedure to save object to core data.
Now, I'm getting duplicated objects in core data if I restart the setup().
The object has an attribute which is id.
I've thought I could fetch first objects that could eventually already exist in core data, save them to an array (a custom type one), and test for each new object to add to core data if already exist one with the same id.
The code used is the following:
if !existingCards.isEmpty {
for existingCard in existingCards {
if id == existingCard.id {
moc.deleteObject(existingCard)
println("DELETED \(existingCard.name)")
}
}
}
...
// "existingCards is the array of object fetched previously.
// Code to save the object to core data.
Actually, the app return
EXC_BAD_ACCESS(code=1, address Ox0)
Is there an easier way to achieve my purpose or what should I fix to make my code work? I'm quite new to swift and I can't figure other solution.
The main purpose is to delete duplicated core data, BTW.
Swift 4 code to delete duplicate object:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Card")
var resultsArr:[Card] = []
do {
resultsArr = try (mainManagedObjectContext!.fetch(fetchRequest) as! [Card])
} catch {
let fetchError = error as NSError
print(fetchError)
}
if resultsArr.count > 0 {
for x in resultsArr {
if x.id == id {
print("already exist")
mainManagedObjectContext.deleteObject(x)
}
}
}
At the end, I managed to make it work.
I had to rewrite my code, because I realized moc.deleteObject() works with a fetch before, which in my previous code wasn't in the same function, but it was in viewDidLoad().
// DO: - Fetch existing cards
var error: NSError?
var fetchRequest = NSFetchRequest(entityName: "Card")
if let results = moc.executeFetchRequest(fetchRequest, error: &error) as? [Card] {
if !results.isEmpty {
for x in results {
if x.id == id {
println("already exist")
moc.deleteObject(x)
}
}
}
} else {
println(error)
}
No more existingCards, the result of the the fetch is now processed as soon as possible. Something isn't clear to me yet, but now my code works. If you have any improvements/better ways, they're welcome.
P.S.: I actually found Apple reference useful but hard to understand because I don't know Obj-C. Often I can figure what the code do, but in swift functions and properties are a bit different.

Resources