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)
}
})
}
Related
Now I'm developing the function using DispatchQueue and Cloud functions.
When I call it, DispatchQueue takes too much time, more than I expected.
And a function in DispatchQueue can't wait for the result from cloud functions.
Find the code below:
func createGroup(members: [String]) {
guard let userId = Auth.auth().currentUser?.uid else { return }
var ref: DocumentReference? = nil
let data =
[
"members": []
] as [String: Any]
ref = COLLECTION_GROUP.addDocument(data: data) {
err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(ref!.documentID)")
if !members.isEmpty {
self.provisionNewAccount(members: members)
}
}
}
}
func provisionNewAccount(members: [String]) {
DispatchQueue.main.async {
members.forEach { member in
self.functions.httpsCallable("hello").call(["email": member]) { (result, error) in
if let error = error as NSError? {
if error.domain == FunctionsErrorDomain {
let code = FunctionsErrorCode(rawValue: error.code)
let message = error.localizedDescription
let details = error.userInfo[FunctionsErrorDetailsKey]
print(code)
print(message)
print(details)
}
}
print(result?.data)
if let data = (result?.data as? [String: Any]), let text = data["result"] as? String {
print("SUCCESS: \(text)")
}
}
}
}
}
I'm trying to practice async/await with PromiseKit !
Here is my original function
func getProductDetailsList(stationId: String?) {
guard let productID = productID else {
return
}
getProductDetails(productID: productID)
.done { [weak self] products -> Void in
guard let firstProduct = products.first else {
throw DetailsEmptyError()
}
product?.model.isAvailable = true
self?.delegate?.didSucceedLoadingDetails()
}
.ensure {
self.delegate?.didEnsureLoadingDetails()
}
.catch { [weak self] _ in
self?.delegate?.didFailLoadingDetails()
}
}
And here how I tried to introduce async/await
func getProductDetailsList(stationId: String?) {
guard let productID = productID else {
return
}
let products = await getProductsDetails(productsIDs: productID)
guard let firstProduct = products.first else {
throw DetailsEmptyError()
}
product?.model.isAvailable = true
self?.delegate?.didSucceedLoadingDetails()
.ensure {
self.delegate?.didEnsureLoadingDetails()
}
.catch { [weak self] _ in
self?.delegate?.didFailLoadingDetails()
}
}
and here is my getProductsDetails function
func getProductsDetails(productsIDs: productID) async -> Promise<[ProductDetails?]> {
let promises = Set(productsIDs.compactMap { $0 })
.map { ProductDetails(id: $0) }
return when(resolved: promises)
.map { _ in
return productsIDs.map { productsID in promises.first(where: { $0.field?.id == productsID })?.field }
}
}
I don't know if this is the right path to follow in order to integrate async/await into an existing code + How can deal with the part of .ensure and .catch ?
Thanks !
I have two snapshot listeners and I need to run them in same completion block to get data to same array on first time when application starts. After application is started and listeners are listening I need to run functions separately. I cannot use completion block because if data changes on fetchOwnGames function it also calls another fetchFriendsGames function.
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
self.fetchOwnGames {
self.fetchFriendsGames {
completion()
}
}
}
}
Also I cannot use dispatchGroup because if function completion called dispatchGroup.leave() function is getting error
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
fetchOwnGames {
dispatchGroup.leave()
}
dispatchGroup.enter()
fetchFriendsGames {
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion()
}
}
}
How I can call functions separately but data comes same time.
Here is my fetchOwnGames and fetchFriendGames functions
func fetchOwnGames(completion: #escaping () -> Void) {
guard let ownUid = UserService.shared.currentUser?.id else { return }
ownListener = Constants.FirebaseCollection.gamesCollection
.order(by: "startTime")
.whereField("ownerUid", isEqualTo: ownUid)
.limit(toLast: 5)
.addSnapshotListener { [self] querySnapshot, error in
guard let querySnapshot = querySnapshot, error == nil else {
print("DEBUG: error", error?.localizedDescription as Any)
return
}
querySnapshot.documentChanges.forEach { (change) in
switch change.type {
case .added:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games.append(data)
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
self.ownGames.append(data)
SettingsManager.shared.gamesCount = ownGames.count
case .modified:
guard let data = try? change.document.data(as: Game.self) else { return }
if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) {
self.games[index] = data
}
case .removed:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games = self.games.filter { $0 != data }
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
SettingsManager.shared.gamesCount = games.count
}
}
print("DEBUG2: owngames count", ownGames.count)
completion()
}
}
func fetchFriendsGames(completion: #escaping () -> Void) {
userService.fetchFriends(friendCompletion: { [self] friends in
let friendsUidArray = friends.map { $0.id }
if friendsUidArray.count == 0 {
completion()
} else {
for uid in friendsUidArray {
guard let uid = uid else { return }
friendListener = Constants.FirebaseCollection.gamesCollection
.order(by: "startTime", descending: true)
.whereField("ownerUid", isEqualTo: uid)
.limit(to: 5)
.addSnapshotListener({ querySnapshot, error in
guard let querySnapshot = querySnapshot, error == nil else {
print("DEBUG: error", error?.localizedDescription as Any)
return
}
querySnapshot.documentChanges.forEach { change in
switch change.type {
case .added:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games.append(data)
self.friendGames.append(data)
self.games = games.sorted(by: { $0.startTime.compare($1.startTime) == .orderedDescending})
case .modified:
guard let data = try? change.document.data(as: Game.self) else { return }
if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) {
self.games[index] = data
}
case .removed:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games = self.games.filter { $0 != data }
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
}
}
print("DEBUG3: friendgames count", friendGames.count)
completion()
})
}
}
})
}
Got it work with adding boolean checker.
private var appStarted = false
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
fetchOwnGames {
if !self.appStarted {
dispatchGroup.leave()
}
}
dispatchGroup.enter()
fetchFriendsGames {
if !self.appStarted {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.appStarted = true
completion()
}
}
}
Right now I have to call the function (calculatePortfolioGrossBalance) 3 times for the value to update, what am I doing wrong in the state logic?
In the code below, when I call in an init the function calculatePortfolioGrossBalance() it returns empty [], I have to call it 3 times for the value to update, However... if I print the values of getTokenBalancesModel in the line DispatchQueue.main.async { I can see the values are there, so how come in calculatePortfolioGrossBalance are not?
final class TokenBalancesClassAViewModel: ObservableObject {
#Published var getTokenBalancesModel: [TokenBalancesItemsModel] = [TokenBalancesItemsModel]()
#Published var portfolioGrossBalance: String = "0.0"
func calculatePortfolioGrossBalance() {
getTokenBalances()
DispatchQueue.main.async {
var totalBalance: Double = 0
for item in self.getTokenBalancesModel {
totalBalance += Double(item.quote!)
}
self.portfolioGrossBalance = String(format:"%.2f", totalBalance)
print(self.portfolioGrossBalance)
}
}
func getTokenBalances() {
guard let url = URL(string: "someUrlHeidiGaveMe") else {
print("Invalid URL")
return
}
print("Calling getTokenBalances() ...")
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalancesModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
}
} catch {
print("ERROR:", error)
}
})
}
}
you need to read up on using asynchronous functions, how to set them up and how to use them. This is important. Try something like this (untested):
final class TokenBalancesClassAViewModel: ObservableObject {
#Published var getTokenBalancesModel: [TokenBalancesItemsModel] = [TokenBalancesItemsModel]()
#Published var portfolioGrossBalance: String = "0.0"
func calculatePortfolioGrossBalance() {
getTokenBalances() { isGood in
if isGood {
var totalBalance: Double = 0
for item in self.getTokenBalancesModel {
totalBalance += Double(item.quote!)
}
self.portfolioGrossBalance = String(format:"%.2f", totalBalance)
print(self.portfolioGrossBalance)
}
}
}
func getTokenBalances(completion: #escaping (Bool) -> Void) {
guard let url = URL(string: "someUrlHeidiGaveMe") else {
print("Invalid URL")
completion(false)
return
}
print("Calling getTokenBalances() ...")
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
completion(false)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalancesModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
completion(true)
}
} catch {
print("ERROR:", error)
completion(false)
}
})
}
}
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.