im trying to populate two arrays with the data i get from the firestore database. im getting the data successfully however it was late and when i printed them in viewDidLoad it printed empty arrays. so i decided to implement a completion handler however it still shows and empty array. can anyone tell me why my print statement runs before the functions even though im using escaping
func yourFunctionName(finished: #escaping () -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries.append(document.documentID)
}
}
}
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries2.append(document.documentID)
}
}
}
finished()
}
viewDidLoad(){
yourFunctionName {
print(self.countries)
print(self.countries2)
}
}
i get the empty arrays in the output although the arrays should have been filled before i called print though im using #escaping. please someone help me here
You are actually not escaping the closure.
For what I know the "#escaping" is a tag that the developper of a function use to signify the person using the function that the closure he/she is passing will be stored and call later (after the function ends) for asynchronicity and memory management. In your case you call the closure passed immediately in the function itself. Hence the closure is not escaping.
Also the firebase database is asynchronous. Meaning that you don't receive the result immediately
This part :
{ (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries.append(document.documentID)
}
}
}
is itself a closure, that will be executed later when the result of the query is produced. As you can see in the doc, the function is escaping the closure : https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query.html#getdocumentssource:completion:
func getDocuments(source: FirestoreSource, completion: #escaping FIRQuerySnapshotBlock)
So to summarise :
The code for the firebase query will be call later (but you don't know when), and your closure "finished" is called immediately after having define the firebase callback, thus before it has been called.
You should call your finished closure inside the firebase callback to have it when the arrays are populated.
I think your main problem here is not about to populate your arrays, your problem is how to get it better.
I did an example of how you could do that in a better way.
First, break your big function in two, and populate it out of your function.
Look at this code and observe the viewDidLoad implementation.
func countries(withCapital capital: String, completionHandler: (Result<Int, Error>) -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: capital)
.getDocuments { (snapshot, error) in
guard error == nil else {
completionHandler(.failure(error!))
return
}
let documents = snapshot!.documents
let ids = documents.map { $0.documentID }
completionHandler(.success(ids))
}
}
func countries(withClimate climate: String, completionHandler: (Result<Int, Error>) -> Void) {
db.collection("countries")
.whereField("climate", isEqualTo: climate)
.getDocuments { (snapshot, error) in
guard error == nil else {
completionHandler(.failure(error!))
return
}
let documents = snapshot!.documents
let ids = documents.map { $0.documentID }
completionHandler(.success(ids))
}
}
func viewDidLoad(){
countries(withClimate: "pleasant") { (result) in
switch result {
case .success(let countries):
print(countries)
self.countries2 = countries
default:
break
}
}
countries(withCapital: "washington") { (result) in
switch result {
case .success(let countries):
print(countries)
self.countries = countries
default:
break
}
}
}
If you have to call on main thread call using it
DispathQueue.main.async {
// code here
}
I hope it helped you.
They are returning empty arrays because Firebase's function is actually asynchronous (meaning it can run after the function "yourFunctionName" has done its work)
in order for it to work as intended (print the filled arrays)
All you need to do is call it inside Firebase's closure itself, like so:
func yourFunctionName(finished: #escaping () -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
self.countries.append(document.documentID)
finished() //<<< here
}
}
}
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
self.countries2.append(document.documentID)
finished() //<<< and here
}
}
}
}
I has been sometime since I encountered that problem but I beleve the issue is that you are calling the completion handler to late. What I mean is that you can try to call it directly after you have lopped throught the documents. One idea could be to return it in the compltion or just do as you do. Try this instead:
func yourFunctionName(finished: #escaping ([YourDataType]?) -> Void) {
var countires: [Your Data Type] = []
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
countries.append(document.documentID)
}
finished(countries)
return
}
}
}
func yourSecondName(finished: #escaping([YouDataType]?) -> Void) {
var countries: [Your data type] = []
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
//print(document.documentID)
//print(documentData)
countires.append(document.documentID)
}
finished(countires)
return
}
}
func load() {
yourFunctionName() { countries in
print(countires)
}
yourSecondName() { countries in
print(countries)
}
}
viewDidLoad(){
load()
}
What this will do is that when you call the completion block that is of type #escaping as well as returning after it you won't respond to that completely block any longer and therefore will just use the data received and therefore not care about that function anymore.
I good practice according to me, is to return the object in the completion block and use separate functions to be easier to debug and more efficient as well does it let you return using #escaping and after that return.
You can use a separate method as I showed to combine both methods and to update the UI. If you are going to update the UI remember to fetch the main queue using:
DispathQueue.main.async {
// Update the UI here
}
That should work. Greate question and hope it helps!
Related
Goal
Create a function that listens to changes in Firestore and publishes the result or the error
The code
func observe<T: Codable>(document: String, inCollection collection: String) -> AnyPublisher<T, Error> {
let docRef = self.db.collection(collection).document(document)
return Future<T, Error> { promise in
let docRef = self.db.collection(collection).document(document)
let listener = docRef.addSnapshotListener { (snapshot, error) in
guard let object = T(dictionary: snapshot?.data()), error == nil else {
promise(.failure(error ?? CRUDServiceError.encodingError))
return
}
promise(.success(object))
}
// Cancel the listener when the publisher is deallocated
let cancellable = AnyCancellable {
listener.remove()
}
}.eraseToAnyPublisher()
}
Future by define produces only a single value, not suitable for subscribing. PassThroughSubject inside the function failed also.
Error leads to publisher completion. We want to keep listening to changes even after the error is received, I found multiple approaches to achieve this, but they all require specific code on subscribing. I want to handle this problem one time inside the observe function. You can read some solutions here
Check out this gist: https://gist.github.com/IanKeen/934258d2d5193160391e14d25e54b084
With the above gist you can then do:
func observe<T: Codable>(document: String, inCollection collection: String) -> AnyPublisher<T, Error> {
let docRef = self.db.collection(collection).document(document)
return AnyPublisher { subscriber in
let docRef = self.db.collection(collection).document(document)
let listener = docRef.addSnapshotListener { (snapshot, error) in
guard let object = T(dictionary: snapshot?.data()), error == nil else {
subscriber.send(completion: .failure(error ?? CRUDServiceError.encodingError))
return
}
subscriber.send(object)
}
return AnyCancellable { listener.remove() }
}
}
As for the error... A Publisher cannot emit any more values after it emits an error. This is a fundamental part of the contract. Then best you can do is convert the error into some other type and emit as a next event instead of a completion event.
Something like this would do it:
func observe<T: Codable>(document: String, inCollection collection: String) -> AnyPublisher<Result<T, Error>, Never> {
let docRef = self.db.collection(collection).document(document)
return AnyPublisher { subscriber in
let docRef = self.db.collection(collection).document(document)
let listener = docRef.addSnapshotListener { (snapshot, error) in
guard let object = T(dictionary: snapshot?.data()), error == nil else {
subscriber.send(.failure(error ?? CRUDServiceError.encodingError))
return
}
subscriber.send(.success(object))
}
return AnyCancellable { listener.remove() }
}
}
But I suspect that once Firestore send an error to the callback, it too will stop calling your callback with new snapshots... So I don't think this is actually useful.
Here is my code. I have extra entities with nil attributes in Core Data. when I delete and run application firstly, I get one saved object with nil attributes fetched from core data.
class RepositoryEntity {
private var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func fetchRepositories() -> [RepositoryEntity] {
do {
return try context.fetch(RepositoryEntity.fetchRequest())
} catch(let error) {
print("errr: ", error.localizedDescription)
}
return []
}
func saveObject(repo: Repository, onSuccess: () -> Void, onFailure: (_ error: String) -> Void) {
let repoEntity = RepositoryEntity(context: self.context)
repoEntity.fullName = repo.fullName
repoEntity.dateCreated = repo.dateCreated
repoEntity.url = repo.url
repoEntity.language = repo.language
repoEntity.repoDescription = repo.repoDescription
repoEntity.id = repo.id
let ownerEntity = OwnerEntity(context: self.context)
ownerEntity.ownerName = repo.owner.ownerName
ownerEntity.avatarUrl = repo.owner.avatarUrl
repoEntity.addToOwner(ownerEntity)
// Save the data
do {
try context.save()
onSuccess()
} catch(let error) {
onFailure("Something Happend. Try again later.")
print(error.localizedDescription)
}
}
func deleteRepository(repo: Repository, onSuccess: () -> Void, onFailure: (_ error: String) -> Void) {
let repositories = fetchRepositories()
guard let deletableRepo = repositories.first(where: {$0.id == repo.id}) else { return }
self.context.delete(deletableRepo)
do {
try context.save()
onSuccess()
} catch(let error) {
onFailure("Something Happens. try again later.")
print(error.localizedDescription)
}
}
}
when I delete and run application firstly, I get one saved object with nil attributes fetched from core data.
When you write "I get one saved object...": what object? RepositoryEntity? How do you know you have a saved object, by calling fetchRepositories()? I can only assume it's like that (as opposed to having an empty OwnerEntity).
In that case, the problem is that, to call func fetchRepositories() you need to create an instance. So, when you start with zero objects, as soon as you call fetchRepositories() you already have at least one.
Change:
func fetchRepositories() -> [RepositoryEntity]
with:
static func fetchRepositories() -> [RepositoryEntity]
and call it from the type:
RepositoryEntity.fetchRepositories()
The same also for deleteRepository.
I am trying to get documents from a FireStore database. I need these documents to be loaded before moving forward in my function. Here is the code for reference:
The view controller calling the FireStore service functions:
let service = FirestoreServices()
service.get(cottage: "test123456789") { model in
nextViewController.cottageModel = model!
self.present(nextViewController, animated:true, completion:nil)
}
The FireStore service method being called:
func get(cottage: String, completionHandler: #escaping (CottageTrip?) -> ()) {
//get a reference to the firestore
let db = Firestore.firestore()
//get the references to the collections
let cottageRef = db.collection("cottages").document(cottage)
//get the fields from the initial document and load them into the cottage model
cottageRef.getDocument { (document, error) in
if let document = document, document.exists {
//create the cottage model to return
let cottageModel: CottageTrip = CottageTrip()
//get the subcollections of this document
let attendeesCollection = cottageRef.collection("attendees")
//other collections
//here I get all info from initial document and store it in the model
let group = DispatchGroup()
print("here")
group.enter()
//get the attendees
DispatchQueue.global(qos: .userInitiated).async {
attendeesCollection.getDocuments() { (querySnapshot, err) in
print("here2")
if let err = err {
print("Error getting documents: \(err)")
} else {
//get data
}
group.leave()
}
}
print("after async call")
//wait here until the attendees list is built
group.wait()
print("after wait")
//create the cars
carsCollection.getDocuments() { (querySnapshot, err) in
print("in car collection get doc call")
if let err = err {
print("Error getting documents: \(err)")
} else {
//get car data
}
}
}
//this is where she should block until all previous get document operations complete
completionHandler(cottageModel)
} else {
print("Document does not exist")
completionHandler(nil)
}
}
}
I am realizing that the print("here2") never prints so it seems like it blocks on the group.wait(). I need to use group.wait() rather than a notify because I need this function to access subcollections and documents only after the attendees collection is loaded as I need these values for the subcollections and documents. I have read a lot of answers online and most people use group.wait() in this scenario but for some reason I can not get it to work for me without locking and freezing the application.
As algrid pointed out, you have a deadlock because you are waiting on the main thread, which Firestore needs to call its closures.
As a general rule, avoid calling wait and you will not deadlock. Use notify, and just call your closure inside that notify closure.
So, for example, assuming that you do not need the results from attendees in order to query the cars, you can just use a notify dispatch group pattern, e.g.
func get(cottage: String, completionHandler: #escaping (CottageTrip?) -> Void) {
let db = Firestore.firestore()
let cottageRef = db.collection("cottages").document(cottage)
cottageRef.getDocument { document, error in
guard let document = document, document.exists else {
print("Document does not exist")
completionHandler(nil)
return
}
let cottageModel = CottageTrip()
let attendeesCollection = cottageRef.collection("attendees")
let carsCollection = cottageRef.collection("cars")
let group = DispatchGroup()
group.enter()
attendeesCollection.getDocuments() { querySnapshot, err in
defer { group.leave() }
...
}
group.enter()
carsCollection.getDocuments() { querySnapshot, err in
defer { group.leave() }
...
}
group.notify(queue: .main) {
completionHandler(cottageModel)
}
}
}
Also, as an aside, but you do not have to dispatch anything to a global queue, as these methods are already asynchronous.
If you needed the result from one in order to initiate the next, you can just nest them. This will be slower (because you magnify the network latency effect), but also eliminates the need for the group at all:
func get(cottage: String, completionHandler: #escaping (CottageTrip?) -> Void) {
let db = Firestore.firestore()
let cottageRef = db.collection("cottages").document(cottage)
cottageRef.getDocument { document, error in
guard let document = document, document.exists else {
print("Document does not exist")
completionHandler(nil)
return
}
let cottageModel = CottageTrip()
let attendeesCollection = cottageRef.collection("attendees")
let carsCollection = cottageRef.collection("cars")
attendeesCollection.getDocuments() { querySnapshot, err in
...
carsCollection.getDocuments() { querySnapshot, err in
...
completionHandler(cottageModel)
}
}
}
}
Either way, I might be inclined to break this up into separate functions, as it is a little hairy, but the idea would be the same.
I'm currently trying to develop an ios application using Firestore, and when querying the database, I'm getting no results, as neither the if/else block execute. I'm wondering what is going wrong here...
db.collection("users").whereField("uid", isEqualTo: uid).getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error.localizedDescription)")
} else {
for document in querySnapshot!.documents {
weight = document.data()["weight"]! as? Double
}
}
}
Database file structure
Update: I make a call to the database in an earlier method, and this properly returns the user's first name (when I add the weight, it also returns the correct value). But any subsequent calls fail to return anything. Hopefully that info helps.
I have the same Firestore structure like you, and this works for me:
func test() {
var accessLevel: Double?
let db = Firestore.firestore()
db.collection("users").whereField("uid", isEqualTo: UserApi.shared.CURRENT_USER_UID!).getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error.localizedDescription)")
} else {
for document in querySnapshot!.documents {
accessLevel = document.data()["accessLevel"]! as? Double
print(accessLevel!)
}
}
}
}
Current uid:
// Actual logged-in User
var CURRENT_USER_UID: String? {
if let currentUserUid = Auth.auth().currentUser?.uid {
return currentUserUid
}
return nil
}
Hope it helps.
I have an issue with using DispatchGroup (as it was recommended here) with FireStore snapshotListener
In my example I have two functions. The first one is being called by the ViewController and should return array of objects to be displayed in the View.
The second one is a function to get child object from FireStore for each array member. Both of them must be executed asynchronously. The second one should be called in cycle.
So I used DispatchGroup to wait till all executions of the second function are completed to call the UI update. Here is my code (see commented section):
/// Async function returns all tables with active sessions (if any)
class func getTablesWithActiveSessionsAsync(completion: #escaping ([Table], Error?) -> Void) {
let tablesCollection = userData
.collection("Tables")
.order(by: "name", descending: false)
tablesCollection.addSnapshotListener { (snapshot, error) in
var tables = [Table]()
if let error = error {
completion (tables, error)
}
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let firebaseID = document.documentID
let tableName = data["name"] as! String
let tableCapacity = data["capacity"] as! Int16
let table = Table(firebaseID: firebaseID, tableName: tableName, tableCapacity: tableCapacity)
tables.append(table)
}
}
// Get active sessions for each table.
// Run completion only when the last one is processed.
let dispatchGroup = DispatchGroup()
for table in tables {
dispatchGroup.enter()
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(tables, nil)
}
}
}
/// Async function returns table session for table or nil if no active session is opened.
class func getActiveTableSessionAsync (forTable table: Table, completion: #escaping (TableSession?, Error?) -> Void) {
let tableSessionCollection = userData
.collection("Tables")
.document(table.firebaseID!)
.collection("ActiveSessions")
tableSessionCollection.addSnapshotListener { (snapshot, error) in
if let error = error {
completion(nil, error)
return
}
if let snapshot = snapshot {
guard snapshot.documents.count != 0 else { completion(nil, error); return }
// some other code
}
completion(nil,nil)
}
}
Everything works fine till the moment when the snapshot is changed because of using a snapshotListener in the second function. When data is changed, the following closure is getting called:
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
And it fails on the dispatchGroup.leave() step, because at the moment group is empty.
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
All dispatchGroup.enter() and dispatchGroup.leave() are already done on this step. And this closure was called by listener separately.
I tried to find the way how to check if the DispatchGroup is empty to do not call leave() method. But did not find any native solution.
The only similar solution I've found is in the following answer. But it looks too hacky and not sure if will work properly.
Is there any way to check if DispatchGroup is empty? According to this answer, there is no way to do it. But probably something changed during last 2 years.
Is there any other way to fix this issue and keep snapshotListener in place?
For now I implemented some kind of workaround solution - to use a counter.
I do not feel it's the best solution, but at least work for now.
// Get active sessions for each table.
// Run completion only when the last one is processed.
var counter = tables.count
for table in tables {
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
counter = counter - 1
if (counter <= 0) {
completion(tables, nil)
}
})
}