Multiple requests with DispatchQueue.main.async not executing properly in swift 3 - ios

I have a JSON through which I get list of boards. Can be accessed by self.jsonGame.boards. Now I have to call all these boards and display the content from it.
But the boards are not consistently called. They’ll only show up sometimes.
func fetchBoard(){
let repo = GameRepository()
let prefs = UserDefaults.standard
if self.jsonGame.boards.count > 0 {
self.sortedBoardArr.reserveCapacity(self.BoardArr.count)
for board in self.jsonGame.boards{
DispatchQueue.main.async
{
repo.GetBoardInfo(gameID: self.jsonGame.Id, boardID: board , completion : {(response , errorCode ) -> Void in
if errorCode == ErrorCode.NoError{
DispatchQueue.main.sync {
self.BoardArr.append(response)
self.sortArr()
self.collectionView.reloadData()
}
}
})
}
}
}
}
func sortArr(){
if self.jsonGame.boards.count == self.BoardArr.count{
for board in self.jsonGame.boards{
for boardarr in self.BoardArr{
if boardarr.id == board{
self.sortedBoardArr.append(boardarr)
}
}
}
}
}
If anyone can help me figure out how to make sure the boards are called with a consistency.
I'm noob at async handling. Sorry for the trouble.

I had a similar issue, where I populated an array with elements coming from different asynchronous network requests and when the requests were running concurrently, the final size of my array depended on the execution of the concurrent tasks.
I managed to solve my problem using a serial queue and Dispatch Groups.
This is how I would change your code:
func fetchBoard(){
let repo = GameRepository()
let prefs = UserDefaults.standard
if self.jsonGame.boards.count > 0 {
self.sortedBoardArr.reserveCapacity(self.BoardArr.count)
let serialQueue = DispatchQueue(label: "serialQueue")
let group = DispatchGroup()
for board in self.jsonGame.boards{
group.enter()
serialQueue.async {
repo.GetBoardInfo(gameID: self.jsonGame.Id, boardID: board , completion : {(response , errorCode ) -> Void in
if errorCode == ErrorCode.NoError{
self.BoardArr.append(response)
}
group.leave()
})
DispatchQueue.main.async{
group.wait()
self.sortArr()
self.collectionView.reloadData()
}
}
}
}
}
These 2 answers for similar questions here on stack overflow were quite helpful for me when I had a similar issue: Blocking async requests and Concurrent and serial queues

Related

Firebase function not getting called inside a forEach loop with a DispatchGroup

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))
}
}

iOS - SwiftUI - Navigate to the next screen AFTER async actions performed

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

Wait for DispatchQueue [duplicate]

How could I make my code wait until the task in DispatchQueue finishes? Does it need any CompletionHandler or something?
func myFunction() {
var a: Int?
DispatchQueue.main.async {
var b: Int = 3
a = b
}
// wait until the task finishes, then print
print(a) // - this will contain nil, of course, because it
// will execute before the code above
}
I'm using Xcode 8.2 and writing in Swift 3.
If you need to hide the asynchronous nature of myFunction from the caller, use DispatchGroups to achieve this. Otherwise, use a completion block. Find samples for both below.
DispatchGroup Sample
You can either get notified when the group's enter() and leave() calls are balanced:
func myFunction() {
var a = 0
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
a = 1
group.leave()
}
// does not wait. But the code in notify() is executed
// after enter() and leave() calls are balanced
group.notify(queue: .main) {
print(a)
}
}
or you can wait:
func myFunction() {
var a = 0
let group = DispatchGroup()
group.enter()
// avoid deadlocks by not using .main queue here
DispatchQueue.global(qos: .default).async {
a = 1
group.leave()
}
// wait ...
group.wait()
print(a) // you could also `return a` here
}
Note: group.wait() blocks the current queue (probably the main queue in your case), so you have to dispatch.async on another queue (like in the above sample code) to avoid a deadlock.
Completion Block Sample
func myFunction(completion: #escaping (Int)->()) {
var a = 0
DispatchQueue.main.async {
let b: Int = 1
a = b
completion(a) // call completion after you have the result
}
}
// on caller side:
myFunction { result in
print("result: \(result)")
}
In Swift 3, there is no need for completion handler when DispatchQueue finishes one task.
Furthermore you can achieve your goal in different ways
One way is this:
var a: Int?
let queue = DispatchQueue(label: "com.app.queue")
queue.sync {
for i in 0..<10 {
print("Ⓜ️" , i)
a = i
}
}
print("After Queue \(a)")
It will wait until the loop finishes but in this case your main thread will block.
You can also do the same thing like this:
let myGroup = DispatchGroup()
myGroup.enter()
//// Do your task
myGroup.leave() //// When your task completes
myGroup.notify(queue: DispatchQueue.main) {
////// do your remaining work
}
One last thing: If you want to use completionHandler when your task completes using DispatchQueue, you can use DispatchWorkItem.
Here is an example how to use DispatchWorkItem:
let workItem = DispatchWorkItem {
// Do something
}
let queue = DispatchQueue.global()
queue.async {
workItem.perform()
}
workItem.notify(queue: DispatchQueue.main) {
// Here you can notify you Main thread
}
Swift 5 version of the solution
func myCriticalFunction() {
var value1: String?
var value2: String?
let group = DispatchGroup()
group.enter()
//async operation 1
DispatchQueue.global(qos: .default).async {
// Network calls or some other async task
value1 = //out of async task
group.leave()
}
group.enter()
//async operation 2
DispatchQueue.global(qos: .default).async {
// Network calls or some other async task
value2 = //out of async task
group.leave()
}
group.wait()
print("Value1 \(value1) , Value2 \(value2)")
}
Use dispatch group
dispatchGroup.enter()
FirstOperation(completion: { _ in
dispatchGroup.leave()
})
dispatchGroup.enter()
SecondOperation(completion: { _ in
dispatchGroup.leave()
})
dispatchGroup.wait() // Waits here on this thread until the two operations complete executing.
In Swift 5.5+ you can take advantage of Swift Concurrency which allows to return a value from a closure dispatched to the main thread
func myFunction() async {
var a : Int?
a = await MainActor.run {
let b = 3
return b
}
print(a)
}
Task {
await myFunction()
}
Swift 4
You can use Async Function for these situations. When you use DispatchGroup(),Sometimes deadlock may be occures.
var a: Int?
#objc func myFunction(completion:#escaping (Bool) -> () ) {
DispatchQueue.main.async {
let b: Int = 3
a = b
completion(true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
myFunction { (status) in
if status {
print(self.a!)
}
}
}
Somehow the dispatchGroup enter() and leave() commands above didn't work for my case.
Using sleep(5) in a while loop on the background thread worked for me though. Leaving here in case it helps someone else and it didn't interfere with my other threads.

App freeze with DispatchSemaphore wait()

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
}
}

NSOperationQueue finishes all tasks swift 3

I am trying to achieve NSOperationQueue finishing all tasks operation in swift 3. I create a below demo code and it is working according to my expectation.
func downloadTopStroiesDetails(){
let operationQueue: OperationQueue = OperationQueue()
let operation1 = BlockOperation() {
print("BlockOperation1")
for id in 0...5{
operationQueue.addOperation(downloadArticle(index: id))
}
let operation2 = BlockOperation() {
print("BlockOperation2")
}
operationQueue.addOperation(operation2)
}
operationQueue.addOperation(operation1)
}
func downloadArticle(index:Int) -> Operation {
let operation: Operation = BlockOperation { () -> Void in
print(index)
}
return operation
}
downloadTopStroiesDetails() // start calling
Output :
BlockOperation1
0
1
2
3
4
5
BlockOperation2
But when I call a Web API with Alamofire in downloadArticle method output is different.
func downloadArticle(index:Int) -> Operation {
let operation = BlockOperation(block: {
RequestManager.networkManager.fetchFromNetworkwithID(articleid: index) { (response:Any ,sucess:Bool) in
if sucess{
print(index)
//let art = article.init(json:(response as? json)!)!
// self.saveDataIntoCoreData(data: art)
//self.all_TopArticle.append(art)
}
};
})
return operation
}
Now output :
BlockOperation1
BlockOperation2
0
1
2
3
4
5
What i am doing wrong here ?
Your downloadArticle method is creating a block operation that completes immediately because it in turn performs an asynchronous operation.
You need to prevent the block from reaching the end until the async fetch completes. Using a semaphore would be one solution.
func downloadArticle(index:Int) -> Operation {
let operation = BlockOperation(block: {
let semaphore = DispatchSemaphore(value: 0)
RequestManager.networkManager.fetchFromNetworkwithID(articleid: index) { (response:Any ,sucess:Bool) in
if sucess{
print(index)
//let art = article.init(json:(response as? json)!)!
// self.saveDataIntoCoreData(data: art)
//self.all_TopArticle.append(art)
}
semaphore.signal()
};
semaphore.wait()
})
return operation
}
The use of this semaphore ensure the operation doesn't actually complete until the network fetch is also complete.
You might also want to make your operation queue serial instead of concurrent to ensure you only allow one operation to run at a time. If this is what you want, then set the operation queue's maxConcurrentOperationCount to 1.

Resources