I have created a function getFriends that reads a User's friendlist from firestore and puts each friend in a LocalUser object (which is my custom user class) in order to display the friendlist in a tableview. I need the DispatchSemaphore.wait() because I need the for loop to iterate only when the completion handler inside the for loop is called.
When loading the view, the app freezes. I know that the problem is that semaphore.wait() is called in the main thread. However, from reading DispatchQueue-tutorials I still don't understand how to fix this in my case.
Also: do you see any easier ways to implement what I want to do?
This is my call to the function in viewDidLoad():
self.getFriends() { (friends) in
self.foundFriends = friends
self.friendsTable.reloadData()
}
And the function getFriends:
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
}
}
friendsUID is a dict with each friend's uid as a key and true as the value. Since I only need the keys, I store them in the array friendsIdents. Function getUser searches the passed uid in firestore and creates the corresponding LocalUser (usr). This finally gets appended in friends array.
You should almost never have a semaphore.wait() on the main thread. Unless you expect to wait for < 10ms.
Instead, consider dispatching your friends list processing to a background thread. The background thread can perform the dispatch to your database/api and wait() without blocking the main thread.
Just make sure to use DispatchQueue.main.async {} from that thread if you need to trigger any UI work.
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
DispatchQueue.global(qos: .userInitiated).async {
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
// Insert here a DispatchQueue.main.async {} if you need something to happen
// on the main queue after you are done processing all entries
}
}
Related
I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}
I am pretty new to SwiftUI and with DispatchGroups and DispatchQueues.
I would like to create a Button which processes some server requests and then use the returned data with a CoreML model to predict some score. Once, the score is predicted, then the app can navigate to the next screen
Here is the sequence of actions which need to be done before moving to the next screen
// exemple of sequence of actions
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.name = self.names[self.selectedCompanyIndex]
self.fetchTweets(company: self.arobases[self.selectedCompanyIndex])
self.fetchTweets(company: self.hashes[self.selectedCompanyIndex])
group.leave()
}
group.notify(queue: .main) {
print("done")
}
//function for fetching tweets
func fetchTweets(company: String) {
swifter.searchTweet(
using: company,
lang: "en",
count: 100,
tweetMode: .extended,
success: { (results, metadata) in
var tweets = [TextClassifier1Input]()
for i in 0...99 {
if let tweet = results[i]["full_text"].string {
tweets.append(TextClassifier1Input(text: tweet))
}
}
let searchType = String(company.first!)
self.makePrediction(with: tweets, type: searchType)
}) { (error) in
print("There was an error with the Twitter API: --> ", error)
}
}
//function for making predictions via the coreML model
func makePrediction(with tweets: [TextClassifier1Input], type: String) {
do {
let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
var sentimentScore = 0
for pred in predictions {
if pred.label == "pos" {
sentimentScore += 1
} else if pred.label == "neg" {
sentimentScore -= 1
} else {
print("something sent wrong: --> ", pred.label)
}
}
if type == "#" {
arobaseScore = sentimentScore
} else if type == "#" {
hashScore = sentimentScore
}
} catch {
print("There was an error with the ML model: --> ", error)
}
}
The problem is the navigation is executed on the button click whereas I want the previous actions to be done before.
Can anyone let me know how should I use DispatchGroups and DispatchQueue to run my code in the correct sequence
Thanks in advance for your help
welcome to Stack Overflow.
You are not waiting for swifter.searchTweet to complete before doing the update.
The first step is to add a completion handler to fetchTweets. The completion handler is a way of reporting when fetchTweets has finished the networking.
//function for fetching tweets
func fetchTweets(company: String, completion: #escaping () -> Void) {
// ^^^^^^^^^^ Added completion handler
swifter.searchTweet(
using: company,
lang: "en",
count: 100,
tweetMode: .extended,
success: { (results, metadata) in
var tweets = [TextClassifier1Input]()
for i in 0...99 {
if let tweet = results[i]["full_text"].string {
tweets.append(TextClassifier1Input(text: tweet))
}
}
let searchType = String(company.first!)
self.makePrediction(with: tweets, type: searchType)
completion() // <-- Call completion on success
}) { (error) in
print("There was an error with the Twitter API: --> ", error)
completion() // <-- Call completion on failure
}
}
After that is done, then you can enter and leave the group when the networking is done. The code in notify will then be called. It's in that code block where you need to update the UI.
let group = DispatchGroup()
self.name = self.names[self.selectedCompanyIndex]
group.enter()
self.fetchTweets(company: self.arobases[self.selectedCompanyIndex]) {
group.leave() // <-- leave the group in the completion handler
}
group.enter()
self.fetchTweets(company: self.hashes[self.selectedCompanyIndex]) {
group.leave() // <-- leave the group in the completion handler
}
group.notify(queue: .main) {
print("done")
// BEGIN UPDATING THE UI
// NOTE: Here is where you update the UI.
// END UPDATING THE UI
}
for those using programmatic navigation, trying to navigate before a call to the backend is done will cause all sorts of problems where the view wants to still stay until the call is over and your navigation logic wants to continue
I have read up a lot on this subject but have still been stumped on this specific problem. I have many Firebase calls that rely on each other. This is a kind of simplified example of my code. I had trouble making it any shorter and still getting the point across:
class ScoreUpdater {
static let ref = Database.database().reference()
var userAranking = Int?
var userBranking = Int?
var rankingAreceived = false
var rankingBreceived = false
var sum = 0
// Pass in the current user and the current meme
static func beginUpdate(memeID: String, userID: String) {
// Iterate through each user who has ranked the meme before
ScoreUpdater.ref.child("memes/\(memeID)/rankings")observeSingleEvent(of: .value) {
let enumerator = snapshot.children
while let nextUser = enumerator.nextObject() as? DataSnapshot {
// Create a currentUpdater instance for the current user paired with each other user
let currentUpdater = ScoreUpdater()
This is where the asynchronous calls start. Multiple gatherRankingValues functions can run at one time. This function contains a Firebase call which is asynchronous, which is okay for this function. The updateScores however cannot run until gatherRankingValues is finished. That is why I have the completion handler. I think this area is okay based on my debug printing.
// After gatherRankingValues is finished running,
// then updateScores can run
currentUpdater.gatherRankingValues(userA: userID, userB: nextUser.key as! String) {
currentUpdater, userA, userB in
currentUpdater.updateScores(userA: userA, userB:userB)
}
}
}
}
func gatherRankingValues(userA: String, userB: String, completion: #escaping (_ currentUpdater: SimilarityScoreUpdater, _ userA: String, _ userB: String) -> Void) {
// Iterate through every meme in the database
ScoreUpdater.ref.child("memes").observeSingleEvent(of: .value) {
snapshot in
let enumerator = snapshot.children
while let nextMeme = enumerator.nextObject() as? DataSnapshot {
Here is where the main problem comes in. The self.getRankingA and self.getRankingB never run. Both of these methods need to run before the calculation method. I try to put in the "while rankingReceived == false" loop to keep the calculation from starting. I use the completion handler to notify within the self.rankingAreceived and self.rankingBreceived when the values have been received from the database. Instead, the calculation never happens and the loop becomes infinite.
If I remove the while loop waiting for the rankings to be received, the calculations will be "carried out" except the end result ends up being nil because the getRankingA and getRankingB methods still do not get called.
self.getRankingA(userA: userA, memeID: nextMeme.key) {
self.rankingAreceived = true
}
self.getRankingB(userB: userB, memeID: nextMeme.key) {
self.rankingBreceived = true
}
while self.rankingAreceived == false || self.rankingBreceived == false {
continue
}
self.calculation()
}
So yes, every meme gets looped through before the completion is called, but the rankings don't get called. I can't figure out how to get the loop to wait for the rankings from getRankingA and getRankingB and for the calculation method to run before continuing on to the next meme. I need completion of gatherRankingValues (see below) to be called after the loop has been through all the memes, but each ranking and calculation to complete also before the loop gets called again ... How can I within the getRankingA and getRankingB completion handlers tell the meme iterating loop to wait up?
// After every meme has been looped through for this pair of users, call completion
completion(self, userA, userB)
}
}
function getRankingA(userA: String, memeID: String, completion: #escaping () -> Void) {
ScoreUpdater.ref.child("memes/\(memeID)\rankings\(userA)").observeSingleEvent(of: .value) {
snapshot in
self.userAranking = snapshot.value
completion()
}
}
function getRankingB(userB: String, memeID: String, completion: #escaping () -> Void) {
ScoreUpdater.ref.child("memes/\(memeID)\rankings\(userB)").observeSingleEvent(of: .value) {
snapshot in
self.userBranking = snapshot.value
completion()
}
}
func calculation() {
self.sum = self.userAranking + self.userBranking
self.userAranking = nil
self.userBranking = nil
}
func updateScores() {
ScoreUpdater.ref.child(...)...setValue(self.sum)
}
}
Tomte's answer solved one of my problems (thank you!). The calculations will be carried out after userAranking and userBranking are received with this code:
while let nextMeme = enumerator.nextObject() as? DataSnapshot {
let group = DispatchGroup()
group.enter()
self.getRankingA(userA: userA, memeID: nextMeme.key) {
self.rankingAreceived = true
group.leave()
}
group.enter()
self.getRankingB(userB: userB, memeID: nextMeme.key) {
self.rankingBreceived = true
group.leave()
}
// is called when the last task left the group
group.notify(queue: .main) {
self.calculation()
}
}
Still, the completion call to updateScores would happen at the end of the loop but before all of the userArankings and userBrankings are received and before the rankings undergo calculations. I solved this problem by adding another dispatch group:
let downloadGroup = DispatchGroup()
while let nextMeme = enumerator.nextObject() as? DataSnapshot {
let calculationGroup = DispatchGroup()
downloadGroup.enter()
calculationGroup.enter()
self.getRankingA(userA: userA, memeID: nextMeme.key) {
downloadGroup.leave()
calculationGroup.leave()
}
downloadGroup.enter()
calculationGroup.enter()
self.getRankingB(userB: userB, memeID: nextMeme.key) {
downloadGroup.leave()
calculationGroup.leave()
}
// is called when the last task left the group
downloadGroup.enter()
calculationGroup.notify(queue: .main) {
self.calculation() {
downloadGroup.leave()
}
}
}
downloadGroup.notify(queue: .main) {
completion(self, userA, userB)
}
I had to add a completion handler to the calculation method as well to ensure that the updateScores method would be called once userAranking and userBranking undergo calculations, not just once they are received from the database.
Yay for dispatch groups!
To wait for a loop to complete or better, do stuff after some async call is executed, you could use DispatchGroups. This example shows how they work:
let group = DispatchGroup()
var isExecutedOne = false
var isExecutedTwo = false
group.enter()
myAsyncCallOne() {
isExecutedOne = true
group.leave()
}
group.enter()
myAsyncCallTwo() {
isExecutedOTwo = true
group.leave()
}
group.notify(queue: .main) {
if isExecutedOne && isExecutedTwo {
print("hooray!")
} else {
print("nope...")
}
}
UPDATE
This example shows you how the group is used to control the output. There is no need to wait() or something. You just enter the group in every iteration of the loop, leave it in the async callbacks and when every task left the group, group.notify()is called and you can do the calculations:
while let nextMeme = enumerator.nextObject() as? DataSnapshot {
let group = DispatchGroup()
group.enter()
self.getRankingA(userA: userA, memeID: nextMeme.key) {
self.rankingAreceived = true
group.leave()
}
group.enter()
self.getRankingB(userB: userB, memeID: nextMeme.key) {
self.rankingBreceived = true
group.leave()
}
// is called when the last task left the group
group.notify(queue: .main) {
self.calculation()
}
}
group.notify()is called when all the calls have left the group. You can have nested groups too.
Happy Coding!
user
29384092840923
chatRoomsJoined
chatRoom1
chatroom5
chatrooms
chatRoom1
users
29384092840923
298340982039490
I'm trying to load a tableview with information about the chat rooms a user has joined into. In the case above, user "29384092840923" has joined into chatRoom1, and I need the count of children of the users node in chatRoom1
My initial strat was to get an array of the joinedChatRooms from the "user" node and then do a for loop through and do a getDocument on each of the items in the array.
static func loadFavoriteRooms(forUID uid: String, completedFetch: #escaping (_ favoritedRoomsArray : [String]?, _ error : Error?)->()) {
let userFavoritesRef = database.collection("users").document(uid).collection("favoritedRooms")
userFavoritesRef.getDocuments { (snapshot, error) in
if error != nil {
completedFetch(nil, error!)
print("There was an error", error!.localizedDescription)
} else {
var roomArray = [String]()
for document in snapshot!.documents {
//Create a roomRef with the documentID, do a getDocument with it, and create an object with it?
let roomName = document.documentID
roomArray.append(roomName)
}
completedFetch(roomArray, nil)
}
}
}
My problem with what happened above was once I started sending off additional getDocument requests within the for-loop for the individual roomRefs, my completedFetch completion call was returning before the for loop was done asynchronously, and I wasn't getting a filled array back.
What's the cleanest way to do this? Do I need to do a dispatch group here or is there a better way to accomplish this? Using dispatch groups with firestore seems wrong here to me for some reason.
One possible option could be to use a DispatchGroup. Something like -
var roomArray = [String]()
let dispatchGroup = DispatchGroup()
for document in snapshot!.documents {
let roomId = document.documentID
let roomRef = database.collection("rooms").document(roomId)
dispatchGroup.enter()
roomRef.getDocument { (roomSnapshot, error) in
// Create the room from the snapshot here
roomArray.append(roomName)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main, execute: {
completedFetch(roomArray, nil)
})
Just make sure you got your .enter() and .leave() calls correct otherwise you'll get some very strange crashes.
I am receiving up to four push notifications for each event I am subscribed to. I have gone through everything related to my CloudKit subscriptions and notification registry and I am convinced this is an Apple problem. I have instead turned my attention toward correctly processing the notifications no matter how many I receive. Here is a simplified version of what I am doing:
func recievePrivatePush(_ pushInfo: [String:NSObject], completion: #escaping ()->Void) {
let notification = CKNotification(fromRemoteNotificationDictionary: pushInfo)
let alertBody = notification.alertBody
if let queryNotification = notification as? CKQueryNotification {
let recordID = queryNotification.recordID
guard let body = queryNotification.alertBody else {
return
}
if recordID != nil {
switch body {
case "Notification Type":
let id = queryNotification.recordID
switch queryNotification.queryNotificationReason {
case .recordCreated:
DataCoordinatorInterface.sharedInstance.fetchDataItem(id!.recordName, completion: {
//
})
break
default:
break
}
}
}
}
}
The fetching code looks something like this:
func fetchDataItem(_ id: String, completion: #escaping ()-> Void) {
if entityExistsInCoreData(id) {return}
let db = CKContainer.default().privateCloudDatabase
let recordID = CKRecordID(recordName: id)
db.fetch(withRecordID: recordID) { (record, error) in
if let topic = record {
//Here I create and save the object to core data.
}
completion()
}
}
All of my code works, the problem I am having is that when I receive multiple notifications, multiple fetch requests are started before the first core data entity is created, resulting in redundant core data objects.
What I would like to do is find a way to add the fetch requests to a serial queue so they are processed one at a time. I can put my request calls in a serial queue, but the callbacks always run asynchronously, so multiple fetch requests are still make before the first data object is persisted.
I have tried using semaphores and dispatch groups with a pattern that looks like this:
let semaphore = DispatchSemaphore(value: 1)
func recievePrivatePush(_ pushInfo: [String:NSObject], completion: #escaping ()->Void) {
_ = semaphore.wait(timeout: .distantFuture)
let notification = CKNotification(fromRemoteNotificationDictionary: pushInfo)
let alertBody = notification.alertBody
if let queryNotification = notification as? CKQueryNotification {
let recordID = queryNotification.recordID
guard let body = queryNotification.alertBody else {
return
}
if recordID != nil {
switch body {
case "Notification Type":
let id = queryNotification.recordID
switch queryNotification.queryNotificationReason {
case .recordCreated:
DataCoordinatorInterface.sharedInstance.fetchDataItem(id!.recordName, completion: {
semaphore.signal()
})
break
default:
break
}
}
}
}
}
Once the above function is called for the second time, and semaphore.wait is called, the execution of the first network request pauses, resulting in a frozen app.
Again, what I would like to accomplish it adding the asynchronous network requests to a queue so that they are made only one at a time i.e. the first network call is completed before the second request is started.
Carl,
Perhaps you'll find your solutions with dispatch groups, a few key expressions to look into.
let group = DispatchGroup()
group.enter()
... code ...
group.leave
group.wait()
I use them to limit the number of http requests I send out in a batch, to wait for the response. Perhaps you could use them together with the suggestion in my comment. Watch this video too, dispatch groups in here, I think more.
https://developer.apple.com/videos/play/wwdc2016/720/
These simple classes helped me solve the problem.
class PushQueue {
internal var pushArray: Array<String> = [String]()
internal let pushQueue = DispatchQueue(label: "com.example.pushNotifications")
public func addPush(_ push: Push) {
pushQueue.sync {
if pushArray.contains(push.id) {
return
} else {
pushArray.append(push.id)
processNotification(push: push)
}
}
}
internal func processNotification(push: Push) {
PushInterface.sharedInstance.recievePrivatePush(push.userInfo as! [String: NSObject])
}
}
class CKPush: Equatable {
init(userInfo: [AnyHashable: Any]) {
let ck = userInfo["ck"] as? NSDictionary
let id = ck?["nid"] as? String
self.id = id!
self.userInfo = userInfo
}
var id: String
var userInfo: [AnyHashable:Any]
public static func ==(lhs: CKPush, rhs: CKPush) -> Bool {
return lhs.id == rhs.id ? true : false
}
}
Please ignore the sloppy force unwraps. They need to be cleaned up.