how to fix app freeze after dispatchsemaphore call - swift

I was reading up on this question about app freezes and semaphores and I tried to implement the answer into my code, but it still freezes my app despite calling the UI work on the main thread. My goal is to stop the app from freezing once all the entries are called and have the UI work continue like normal.
This is the alert action I have in the deletion method so far:
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
let semaphore = DispatchSemaphore(value: 0)
self.deleteButton.isHidden = true
self.loadingToDelete.alpha = 1
self.loadingToDelete.startAnimating()
DispatchQueue.global(qos: .userInitiated).async {
self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The docs couldn't be retrieved for deletion.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user being deleted has no events purchased.")
return
}
for document in querySnapshot!.documents {
let docID = document.documentID
self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
guard querySnap?.isEmpty == false else {
print("The user being deleted has no guests with his purchases.")
return
}
for doc in querySnap!.documents {
let guest = doc.documentID
self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
guard error == nil else {
print("Error deleting guests while deleting user.")
return
}
print("Guests deleted while deleting user!")
semaphore.signal()
}
semaphore.wait()
}
}
}
}
self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("There was an error retrieving docs for user deletion.")
return
}
guard querySnapshot?.isEmpty == false else {
return
}
for document in querySnapshot!.documents {
let docID = document.documentID
self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
guard err == nil else {
print("There was an error deleting the the purchased events for the user being deleted.")
return
}
print("Purchases have been deleted for deleted user!")
semaphore.signal()
}
semaphore.wait()
}
}
self.db.document("student_users/\(user.uid)").delete(completion: { (error) in
guard error == nil else {
print("There was an error deleting the user document.")
return
}
print("User doc deleted!")
semaphore.signal()
})
semaphore.wait()
user.delete(completion: { (error) in
guard error == nil else {
print("There was an error deleting user from the system.")
return
}
print("User Deleted.")
semaphore.signal()
})
semaphore.wait()
DispatchQueue.main.async {
self.loadingToDelete.stopAnimating()
self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
}
}
}
So this actually deletes everything cleanly with no residual data in the Firestore database, which is what I wanted to happen all along, the only issue is that the app freezes. I thought that the answer in the question I linked above would work in my case, but it didn't.
Also to mention, I've had suggestions of using Cloud Functions for this issue but my app has two types of users with different logic and syntax in the deletion process so I couldn't just use a simple auth().onDelete() in Cloud Functions and clean up residue. Even if I could, it would be the same issue I'm facing here but just on the server side, trying to order the tasks correctly, which in my opinion is repetitive and not the most sensible thing to do at this point.
Any other suggestions to overcome this issue? Thanks in advance.
EDIT Since semaphores are not the way to go, I resorted to this :
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
self.deleteButton.isHidden = true
self.loadingToDelete.alpha = 1
self.loadingToDelete.startAnimating()
self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The docs couldn't be retrieved for deletion.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user being deleted has no events purchased.")
return
}
for document in querySnapshot!.documents {
let docID = document.documentID
self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
guard querySnap?.isEmpty == false else {
print("The user being deleted has no guests with his purchases.")
return
}
let group = DispatchGroup()
for doc in querySnap!.documents {
let guest = doc.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
guard error == nil else {
print("Error deleting guests while deleting user.")
return
}
print("Guests deleted while deleting user!")
group.leave()
}
}
}
}
}
self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("There was an error retrieving docs for user deletion.")
return
}
guard querySnapshot?.isEmpty == false else {
return
}
let group = DispatchGroup()
for document in querySnapshot!.documents {
let docID = document.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
guard err == nil else {
print("There was an error deleting the the purchased events for the user being deleted.")
return
}
print("Purchases have been deleted for deleted user!")
group.leave()
}
}
}
self.db.collection("student_users").whereField("userID", isEqualTo: user.uid).getDocuments { (querySnapshot, error) in
guard error == nil else {
print("There was an error deleting the user document.")
return
}
guard querySnapshot?.isEmpty == false else {
return
}
let group = DispatchGroup()
for document in querySnapshot!.documents {
let docID = document.documentID
group.enter()
self.db.document("student_users/\(docID)").delete { (err) in
guard err == nil else {
return
}
print("User doc deleted!")
group.leave()
}
}
}
let group = DispatchGroup()
group.enter()
user.delete(completion: { (error) in
guard error == nil else {
print("There was an error deleting user from the system.")
return
}
print("User Deleted.")
group.leave()
})
group.notify(queue: .main) {
self.loadingToDelete.stopAnimating()
self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
}
}
This still leaves residual data and does not execute the tasks in order. Any other suggestions?

Let me give you some ideas because I think your solution should incorporate some or all of these. First is how dispatch groups work and how you can nest them to execute blocks of async tasks in order:
func deleteUser(completion: #escaping (_ done: Bool) -> Void) {
// put UI into loading state
db.collection("someCollection").getDocuments { (snapshot, error) in
if let snapshot = snapshot {
if snapshot.isEmpty {
completion(true) // no errors, nothing to delete
} else {
let dispatchGroup = DispatchGroup() // instantiate the group outside the loop
var hasErrors = false
for doc in snapshot.documents {
dispatchGroup.enter() // enter on every iteration
db.document("someDocument").delete { (error) in
if let error = error {
print(error)
hasErrors = true
}
dispatchGroup.leave() // leave on every iteration regardless of outcome
}
}
dispatchGroup.notify(queue: .main) {
if hasErrors {
completion(false) // failed to delete
} else {
// execute next task and repeat
}
}
}
} else {
if let error = error {
print(error)
completion(false) // failed to delete
}
}
}
}
deleteUser { (done) in
if done {
// segue to next view controller
} else {
// retry or alert user
}
}
The example above is the basics of how dispatch group can work for you. When you leave the group the same number of times you've entered it, the completion handler is called. This example does not have any recursion and doesn't check if everything was actually deleted. Here is an example of how you could add some of that:
func deleteUser(completion: #escaping (_ done: Bool) -> Void) {
var retries = 0
func task() {
db.collection("someCollection").getDocuments { (snapshot, error) in
if let snapshot = snapshot {
if snapshot.isEmpty {
completion(true) // done, nothing left to delete
} else {
// delete the documents using a dispatch group or a Firestore batch delete
task() // call task again when this finishes
// because this function only exits when there is nothing left to delete
// or there have been too many failed attempts
}
} else {
if let error = error {
print(error)
}
retries += 1 // increment retries
run() // retry
}
}
}
func run() {
guard retries < 5 else {
completion(false) // 5 failed attempts, exit function
return
}
if retries == 0 {
task()
} else { // the more failures, the longer we wait until retrying
DispatchQueue.main.asyncAfter(deadline: .now() + Double(retries)) {
task()
}
}
}
run()
}
This doesn't answer your question directly but it should help you with the task overall. You can also forego some of the looping and deleting and do it all inside a Firestore batch operation, which comes with its own completion handler. There are lots of ways to tackle this but these are some things I'd consider.

Related

return value from async Firestore query swift

I am trying to query my Firestore database to see if a desired username is taken. The query works, however I need to return a value if it is empty or not to see if the username already exists. I am trying to use a completion handler but it doesn't seem to work:
func checkUserTaken(cleanUsername: String ,completion:#escaping(String) -> (Void)){
let db = Firestore.firestore()
var userTaken: String = ""
let docRef = db.collection("users").whereField("username", isEqualTo: cleanUsername)
docRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if(querySnapshot!.documents.isEmpty){
print("user is available")
userTaken = "user is available"
}
else{
print("user is taken")
userTaken = "user is taken"
}
}
completion(userTaken)
}
This task is a good way to learn some important and helpful things about Swift, such as naming conventions, deciding what to return from functions (you chose a string, I opted for a boolean), accounting for errors, etc.
func checkUsername(_ username: String, completion: #escaping (_ taken: Bool?) -> Void) {
Firestore.firestore().collection("users").whereField("username", isEqualTo: username).getDocuments() { (snapshot, err) in
if let snapshot = snapshot {
if snapshot.documents.isEmpty {
completion(false)
} else {
completion(true)
}
} else {
if let err = err {
print(err)
}
completion(nil)
}
}
}
Usage
checkUsername("drake") { (taken) in
guard let taken = taken else {
// handle error, maybe retry?
return
}
if taken {
// prompt user username is taken
} else {
// username not taken, proceed
}
}
In the signature of the function, I labeled the boolean in the completion closure (taken), which the Swift compiler does not require you do but I think can be very helpful.
By the way, this function can return anything, even a Result object which is a neat way to return an object or an Error in a single object. But I think returning a boolean is straightforward enough here. I made the boolean an optional so the function can return three possible states (true, false, or nil) to give you a way to handle errors.
String return
func checkUsername(_ username: String, completion: #escaping (_ name: String?) -> Void) {
Firestore.firestore().collection("users").whereField("username", isEqualTo: username).getDocuments() { (snapshot, err) in
if let snapshot = snapshot {
if snapshot.documents.isEmpty {
completion(username) // return the username if it's available
} else {
completion("") // return an empty string if taken
}
} else {
if let err = err {
print(err)
}
completion(nil) // return nil if error
}
}
}
checkUsername("drake") { (name) in
guard let name = name else {
// handle error
return
}
if name.isEmpty {
// username taken
} else {
print(name) // username not taken
}
}

async deletion task won't work if guest count is more than 1

So my goal is to delete all user's guests if the guest count for a purchased ticket is over 1 when a user is deleting their account.
Currently I have this function to try to accomplish this:
func deleteUserGuests(completion: #escaping (_ done: Bool) -> Void) {
var retries = 0
guard let user = Auth.auth().currentUser else { return }
func checkForGuestsAndDeleteIfAny() {
db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
if let snapshot = querySnapshot {
if snapshot.isEmpty {
completion(true)
// done, nothing left to delete
} else {
// delete the documents using a dispatch group or a Firestore batch delete
for document in querySnapshot!.documents {
let docID = document.documentID
self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
guard querySnap?.isEmpty == false else {
print("The user being deleted has no guests with his purchases.")
return
}
let group = DispatchGroup()
for doc in querySnap!.documents {
let guest = doc.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
guard error == nil else {
print("Error deleting guests while deleting user.")
return
}
print("Guests deleted while deleting user!")
group.leave()
}
}
}
}
checkForGuestsAndDeleteIfAny()// call task again when this finishes
// because this function only exits when there is nothing left to delete
// or there have been too many failed attempts
}
} else {
if let error = error {
print(error)
}
retries += 1 // increment retries
run() // retry
}
}
}
func run() {
guard retries < 30 else {
completion(false) // 5 failed attempts, exit function
return
}
if retries == 0 {
checkForGuestsAndDeleteIfAny()
} else { // the more failures, the longer we wait until retrying
DispatchQueue.main.asyncAfter(deadline: .now() + Double(retries)) {
checkForGuestsAndDeleteIfAny()
}
}
}
run()
}
I upped the retry limit, to see if that was the issue, but it still doesn't delete the guests if there are more than one.
I call it in an alert action when the user successfully reauthenticates before deleting their account:
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
self.deleteButton.isHidden = true
self.loadingToDelete.alpha = 1
self.loadingToDelete.startAnimating()
self.deleteUserGuests { (response) in
if response == false {
return
}
}
self.deleteUserPurchases { (purchase) in
if purchase == false {
return
}
}
self.deleteUserOutOfFirestore { (removed) in
if removed == false {
return
}
}
user.delete(completion: { (error) in
guard error == nil else {
print("There was an error deleting user from the system.")
return
}
print("User Deleted.")
})
self.loadingToDelete.stopAnimating()
self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
}
This is the result in the database:
Everything else gets deleted fine in correct order, purchases, the user itself, and then the user out of Firebase auth, but the guests never get deleted if it is over 1 guest. Is there something I did wrong or left out in the deleteUserGuests method that is causing this issue?
As I've said a number of times, I'd approach this entire task differently--I'd do this sort of cleanup on the server side, perform the deletes atomically using a batch or transaction operation, and have robust recursion throughout. However, to fix your immediate problem of why you can't delete the documents in this subcollection, this will do it.
func deleteUserGuests(completion: #escaping (_ done: Bool) -> Void) {
guard let user = Auth.auth().currentUser else {
return
}
var retries = 0
func task() {
db.collection("student_users/\(user.uid)/events_bought").getDocuments { (snapshot, error) in
if let snapshot = snapshot {
if snapshot.isEmpty {
completion(true)
} else {
let dispatchEvents = DispatchGroup()
var errors = false
for doc in snapshot.documents {
dispatchEvents.enter()
self.db.collection("student_users/\(user.uid)/events_bought/\(doc.documentID)/guests").getDocuments { (snapshot, error) in
if let snapshot = snapshot {
if snapshot.isEmpty {
dispatchEvents.leave()
} else {
let dispatchGuests = DispatchGroup()
for doc in snapshot.documents {
dispatchGuests.enter()
doc.reference.delete { (error) in
if let error = error {
print(error)
errors = true
}
dispatchGuests.leave()
}
}
dispatchGuests.notify(queue: .main) {
dispatchEvents.leave()
}
}
} else {
if let error = error {
print(error)
}
errors = true
dispatchEvents.leave()
}
}
}
dispatchEvents.notify(queue: .main) {
if errors {
retries += 1
run()
} else {
completion(true)
}
}
}
} else {
if let error = error {
print(error)
}
retries += 1
run()
}
}
}
func run() {
guard retries < 30 else {
completion(false)
return
}
if retries == 0 {
task()
} else {
let delay = Double(retries)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
task()
}
}
}
run()
}

Working With Async Firebase Calls SwiftUI

I understand that the Firebase getDocument call is Async, so I'm trying to figure out how to essentially wait until the call finishes executing, and then move on to doing other stuff.
I have tried making use of DispatchGroup() and entering/leaving the group, but I can't seem to get it to work correctly. I have something like the following:
let myGroup = DispatchGroup()
let usersRef = self.db.collection("Users").document("Users").collection("Users")
if self.testCondition == false {
self.errorMessage = "error"
} else{
usersRef.getDocuments {(snap, err) in
myGroup.enter()
//basically getting every username
for document in snap!.documents{
let user = document["username"] as! String
let userRef = usersRef.document(user)
userRef.getDocument { (snapshot, err) in
if err != nil {
print(err)
} else {
let sample = snapshot!["sample"] as! String
if sample == 'bad' {
self.errorMessage = "error"
}
}
}
}
myGroup.leave()
}
print("what4")
//I would like it so that I can execute everything in a code block like this
//after the async call finishes
myGroup.notify(queue: .main) {
print("Finished all requests.")
//THEN DO MORE STUFF
}
}
How can I modify the placement myGroup.enter() and myGroup.leave() in this so that, after the Firebase call has finished, I can continue executing code?
Thanks!
This explains the DispatchGroup() a little bit.
You just have one litte mistake in your code then it should be working.
Make sure to enter() the group outside of the Firebase getDocuments() call. As this already makes the request and takes time thus the process will continue.
This little simple example should help you understand it:
func dispatchGroupExample() {
// Initialize the DispatchGroup
let group = DispatchGroup()
print("starting")
// Enter the group outside of the getDocuments call
group.enter()
let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in
if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
}
// leave the group when done
group.leave()
}
// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
}
}
When waiting for multiple asynchronous calls use completing in the asynchronous function which you let return as soon as you leave the group. Full eg. below:
class Test {
init() {
self.twoNestedAsync()
}
func twoNestedAsync() {
let group = DispatchGroup() // Init DispatchGroup
// First Enter
group.enter()
print("calling first asynch")
self.dispatchGroupExample() { isSucceeded in
// Only leave when dispatchGroup returns the escaping bool
if isSucceeded {
group.leave()
} else {
// returned false
group.leave()
}
}
// Enter second
group.enter()
print("calling second asynch")
self.waitAndReturn(){ isSucceeded in
// Only return once the escaping bool comes back
if isSucceeded {
group.leave()
} else {
//returned false
group.leave()
}
}
group.notify(queue: .main) {
print("all asynch done")
}
}
// Now added escaping bool which gets returned when done
func dispatchGroupExample(completing: #escaping (Bool) -> Void) {
// Initialize the DispatchGroup
let group = DispatchGroup()
print("starting")
// Enter the group outside of the getDocuments call
group.enter()
let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in
if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
// leave the group when succesful and done
group.leave()
}
if let error = error {
// make sure to handle this
completing(false)
group.leave()
}
}
// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
//send escaping bool.
completing(true)
}
}
func waitAndReturn(completing: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
print("Done waiting for 2 seconds")
completing(true)
})
}
}
This gives us the following output:

Is there a way to use Firebase with a ShareExtension without AppGroups

I'm setting up a shareExtension in iOS and want to use the FirebaseSDK to upload data direct instead of using AppGroups. This works as expected, but after 1 hour the UserToken get's invalidated and i can't reach the Firestore Backend anymore.
I'm using the FirebaseSDK (6.2.0) and enabled Keychain sharing to access the current signedIn User. I have the same Google-Plist in the MainApp and the shareExtension. The data gets also uploaded correctly from the shareExtension and was also updated via the snapshotListener in the MainApp.
Relevant code in the MainApp
lazy var db = Firestore.firestore()
//TEAMID form the Apple Developer Portal
let accessGroup = "TEAMID.de.debug.fireAuthExample"
override func viewDidLoad() {
super.viewDidLoad()
do {
try Auth.auth().useUserAccessGroup("\(accessGroup)")
} catch let error as NSError {
print("Error changing user access group: %#", error)
}
guard let user = Auth.auth().currentUser else {
self.statusLabel.text = "user get's lost"
return
}
statusLabel.text = "UserID: \(user.uid)"
// Do any additional setup after loading the view.
db.collection("DummyCollection").addSnapshotListener { (querySnapshot, error) in
if let err = error {
print(err.localizedDescription)
}
guard let snapshot = querySnapshot else {
return
}
DispatchQueue.main.async {
self.dbCountLabel.text = "\(snapshot.count)"
}
}
}
func signIN(){
// https://firebase.google.com/docs/auth/ios/single-sign-on
do {
try Auth.auth().useUserAccessGroup("\(accessGroup)")
} catch let error as NSError {
print("Error changing user access group: %#", error)
}
Auth.auth().signInAnonymously { (result, error) in
if let err = error{
print(err.localizedDescription)
return
}
print("UserID: \(Auth.auth().currentUser!.uid)")
}
}
}
}
Code in the shareExtension:
override func viewDidLoad() {
super.viewDidLoad()
if FirebaseApp.app() == nil {
FirebaseApp.configure()
}
do {
try Auth.auth().useUserAccessGroup(accessGroup)
} catch let error as NSError {
print("Error changing user access group: %#", error)
}
tempUser = Auth.auth().currentUser
if tempUser != nil {
userIDLabel.text = "UserID: \(tempUser!.uid)"
doneButton.isEnabled = true
db.collection("DummyCollection").addSnapshotListener { (querySnapshot, error) in
if let err = error {
print(err.localizedDescription)
}
guard let snapshot = querySnapshot else {
return
}
DispatchQueue.main.async {
self.dataCountLabel.text = "\(snapshot.count)"
}
}
} else {
// No user exists in the access group
self.navigationItem.title = "No User"
}
}
I expect that this should be possible, but the Token gets somehow invalid in the MainApp and i could not reach the Firestore backend.
6.2.0 - [Firebase/Auth][I-AUT000003] Token auto-refresh re-scheduled in 01:00 because of error on previous refresh attempt.
6.2.0 - [Firebase/Firestore][I-FST000001] Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: An internal error has occurred, print and inspect the error details for more information.
Answering my own question: This should be fixed in the next release (Firebase 6.4.0) Details can be found in this PR 3239.

How to notify a queue in Swift (GCD)

I'm using GCD to notify main thread (have 2 async calls inside the function)
My code:
func getWavesByMostRecent(closure: #escaping ([Wave]?) -> Void) {
var waves = [Wave]()
let dispatchGroup = DispatchGroup()
self.query = DatabaseManager.waveRef.queryOrdered(byChild: Constants.reverseTimeStampKey)
self.handle = self.query?.observe(.value, with: { (snapshot) in
for value in snapshot.children {
guard let wave = Wave(snapshot: value as! DataSnapshot) else { return }
self.geoFire = GeoFire(firebaseRef: DatabaseManager.waveRef)
let currentLocation = LocationManager.shared.getCurrentLocation()
dispatchGroup.enter()
self.geoFire?.getLocationForKey(wave.waveID, withCallback: { (location, error) in
guard let location = location else { return }
if error == nil {
if location.distance(from: currentLocation) < Constants.distance {
print("Wave", wave.waveID, "is in range")
waves.append(wave)
} else {
print("Wave", wave.waveID, "is out of range")
}
} else {
print(error?.localizedDescription ?? "")
}
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: .main) {
print("THERE ARE SO MANY WAVES:", waves.count)
closure(waves)
}
})
}
But .notify closure just doesn't work and I cannot call my "main" closure right. What am I doing wrong? Any advice would be appreciated.
Try this change:
self.geoFire?.getLocationForKey(wave.waveID, withCallback: { (location, error) in
defer { dispatchGroup.leave() }
guard let location = location else { return }
if error == nil {
if location.distance(from: currentLocation) < Constants.distance {
print("Wave", wave.waveID, "is in range")
waves.append(wave)
} else {
print("Wave", wave.waveID, "is out of range")
}
} else {
print(error?.localizedDescription ?? "")
}
})
As noted in matt's comment defer is a good tool to do something always when leaving.
This is another issue, but updating an Array from multiple thread simultaneously would cause some problems. It rarely happens, so it can be a hard-to-fix bug.
I'm not sure if GeoFire calls its callback in the main thread or not, but if not, you should better enclose all the callback code in DispatchQueue.main.async {...}.
dispatchGroup.leave() is still in the closure, instead it should be at the end of the for loop like this:
func getWavesByMostRecent(closure: #escaping ([Wave]?) -> Void) {
var waves = [Wave]()
let dispatchGroup = DispatchGroup()
self.query = DatabaseManager.waveRef.queryOrdered(byChild: Constants.reverseTimeStampKey)
self.handle = self.query?.observe(.value, with: { (snapshot) in
for value in snapshot.children {
guard let wave = Wave(snapshot: value as! DataSnapshot) else { return }
self.geoFire = GeoFire(firebaseRef: DatabaseManager.waveRef)
let currentLocation = LocationManager.shared.getCurrentLocation()
dispatchGroup.enter()
self.geoFire?.getLocationForKey(wave.waveID, withCallback: { (location, error) in
guard let location = location else { return }
if error == nil {
if location.distance(from: currentLocation) < Constants.distance {
print("Wave", wave.waveID, "is in range")
waves.append(wave)
} else {
print("Wave", wave.waveID, "is out of range")
}
} else {
print(error?.localizedDescription ?? "")
}
})
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
print("THERE ARE SO MANY WAVES:", waves.count)
closure(waves)
}
})
}