How to make a method execute methods synchronously - swift

I tried using DispatchQueue and DispatchGroup but its still asynchronous, I also tried both dispatchQueue.Async and dispathQueue.sync and neither have worked.
myFunc is a function called in the init() which itself calls 2 functions, getArrOneData() annd getArrTwoData([ArrOneType]).
The first function downloads ArrOneType data from firestore and returns an array which is initialised to a field.
The second function uses the downloaded data/field from the first function to initialise a field in ArrTwoType while simultaneously downloading other relevent ArrTwoType data from firestore to return as an array.
So the Problem is its still asynchronous. How do I use DispatchQueue and DispatchGroup correctly in this scenario?
thanks
note: get arrTwoData is mainly psuedocode
func myFunc(){
let group = DispatchGroup()
let dispatchQueue = DispatchQueue.global(qos: .default)
group.enter()
dispatchQueue.sync {
self.arrOne = self.getArrOneData()//getArrOneData gets data from firestore
group.leave()
}
dispatchQueue.sync {
group.enter()
self.arrTwo = self.getArrTwoData(inputArr: self.arrOne)//getArrTwoData gets data from firestore
group.leave()
}
}
//ArrOneType is an array field in ArrTwoType
func getArrTwoData(inputArr: [ArrOneType]) -> [ArrTwoType]{
var result = [ArrTwoType]()
//retrieving data from firestore, code excluded...
for document in querySnapshot!.documents {
let data = document.data()
name = data["Name"] as? String ?? "Name Unknown"
//returns an array which is a subset of inputArr filtered by name, however
//I dont think the inputArr is populated at this point when I run the program
var field2:[ArrOneType] = someFunc(name, inputArr)
var x = ArrTwoType(name: name, field2: field2)
result.append(x)
}
}
}
return result
}

If you have two requests, one which uses the response from one to prepare the next, the idea is to use #escaping completion handler closures for all asynchronous methods, e.g.
func getArrayOneData(completion: #escaping (Result<[ArrayOneType], Error>) -> Void) {
someAsynchronousMethod {
let values: [ArrayOneType] = ...
completion(.success(values))
}
}
func getArrayTwoData(for typeOneValues: [ArrayOneType], completion: #escaping (Result<[ArrayTwoType], Error>) -> Void) {
someAsynchronousMethod(for: typeOneValues) {
let values: [ArrayTwoType] = ...
completion(.success(values))
}
}
Then you can do things like:
func getEverything(completion: #escaping (Result<[ArrayTwoType], Error>) -> Void) {
getArrayOneData { result in
switch result {
case .failure(let error):
print(error)
case .success(let typeOneValues):
getArrayTwoData(for: typeOneValues) { result in
switch result {
case .failure(let error):
print(error)
case .success(let typeTwoValues):
completion(.success(typeTwoValues))
}
}
}
}
}
Note, no dispatch groups needed. Having added completion handlers to the asynchronous methods, we can just call the second method from the completion handler of the first.

Related

Making multiple requests with Alamofire depending on array of chunks

I'm struggling with making badge requests with Alamofire and I need help.
I have some ids and with them I need to struct parameters (Dictionary String) and send a GET request with Alamofire. Everything is fine, but I need to cover the case when ids are above 200, because when they are more than 200, API returns 414 code status (too long URL). So when ids are more than 200 they are separated in chunks. With each chunk I'm making a new request to API. The problem is that I return only the first 200 ids when I call my method. Here is an example:
func request (_ idsDict: [String: [String]], _ idSchema: String, _ completion: #escaping Result<SomeModel, Error>) -> Void {
let chunks = transformEntitiesIdsToChunks(idsDict)
let decoder = JSONDecoder()
chunks.forEach {chunk in
let parameters = constructQueryParams(idsDict, chunk, idSchema, apiKey, clientId)
AF.request(baseURL, parameters: parameters).response { response in
switch response.result {
case .success(let data):
// some error handling for decoding and no data
completion(.success(data.data))
case .failure(let error):
return completion(.failure(error.localizedDescription))
}
}
}
}
// Method wraps AF request in a continuation block and makes sure that the closure from request method returned data or throwed error.
// That way fetching from API becomes async/await and can be used in do/try/catch block.
func getIdsEntities (_ idsDict: [String: [String]], _ idSchema: String) async throws -> [SomeModel] {
return try await withUnsafeThrowingContinuation { continuation in
request(idsDict, idSchema) { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
return
case .failure(let error):
continuation.resume(throwing: error)
return
}
}
}
}
I have tried with recursive functions and with DispatchGroup but none of them worked. Any help will be appriciated. Thank you in advance.
Thanks to Larme's comment I was able to find my mistake. When making request to API I was passing the decoded response to the completion closure. To fix this I had to declare an array of model let responses:[SomeModel] = [] and append the decoded result to it. I used let group = DispatchGroup() so I can wait the requests to execute and have my final array of results and then I used group.notify(queue: .main, execute: {completion(.success(responses))}) to return to the main queue and have my array of completed fetched data. This is now how my code looks like:
private func request (_ idsDict: [String: [String]], _ idSchema: String, _ completion: #escaping APIListResponseClosure<SomeModel>) -> Void {
var responses: [SomeModel] = []
let group = DispatchGroup()
let chunks = transformEntitiesIdsToChunks(idsDict)
let decoder = JSONDecoder()
chunks.forEach {chunk in
group.enter()
let parameters = constructQueryParams(idsDict, chunk, idSchema, apiKey, clientId)
AF.request(baseURL, parameters: parameters).response { response in
switch response.result {
case .success(let data):
// some error handling for decoding and no data
responses.append(data.data)
group.leave()
case .failure(let error):
return completion(.failure(.APIError(error.localizedDescription)))
}
}
}
group.notify(queue: .main, execute: {
print("Ids are fetched")
completion(.success(responses))
})
}
Thanks again to Larme and I hope I helped someone else with this case.

How to handle priorities in a Swifty JSON Alamofire request?

How can I use the dispatchQueue or something like "await" in Javascript to return a value in self.arrayData (because the end of my loop is ran before the previous content). I am used to R and Python where the code runs line by line, What is the best behavior to adopt in Swift ?
Here is the function :
func fetch2(){
var i:Int = 0
repeat {
AF.request(itemLookUp[i]).validate().responseJSON { response in
switch response.result {
case .failure(let error):
print("\(error) in fetch2")
case .success(let value):
let json = JSON(value)
//Extract the Matiere for ML Extraction
self.matiereInput = json["ResultSet"]["0"]["Result"]["0"]["SpAdditional"].string ?? "none"
let energyCheck:Bool = self.matiereInput.contains("エネルギー") //energy-kcal
if energyCheck==true && self.arrayData[0]==0.0{
//regular expression
var patEnergy = #"(エネルギー)(([^\d]+)(\d+)(\.)(\d+)|([^\d]+)(\d+))"# //avoid the repetition of the pattern within the same matiereinput
let patEnergy2 = self.matches(for: patEnergy, in: self.matiereInput)
patEnergy = patEnergy2.joined(separator:"")
let valueEnergy = self.matches(for: self.regex2, in: patEnergy)
self.arrayData[0] = Double(valueEnergy.joined(separator: "")) ?? 0.0
}
}
}
i = i+1
print(self.arrayData[0])
} while i <= (self.returned-1)
}
Thank you in advance !
The standard pattern is notify with a DispatchGroup, and then use a completion handler to asynchronously notify the caller of the result:
func fetchAll(completion: #escaping (Result<[Double], Error>) -> Void) {
let group = DispatchGroup()
var results: [Double] = []
var errors: [Error] = []
for item in lookupItems {
group.enter() // enter before request
AF.request(item).validate().responseJSON { response in
defer { group.leave() } // leave when this closure is done
switch response.result {
case .failure(let error):
errors.append(error)
case .success(let value):
let result = ...
results.append(result)
}
}
}
group.notify(queue: .main) {
if let error = errors.first { // I don’t know what you want to do if there were multiple errors, so for now I’ll just grab the first one
completion(.failure(error))
} else {
completion(.success(results))
}
}
}
And then you’d use it like so:
fetchAll { result in
switch result {
case .failure(let error):
print(error)
case .success(let values):
print(values)
}
}
Now, I wasn’t able to reverse engineer what you were trying to do (you appear to be updating self.arrayData[0] in every iteration!), so I just returned an array of Double. But you can obviously change the type of results and the parameter of the completion closure to match whatever is relevant in your case.
But don’t get lost in the details of the above example, but rather just focus on a few key observations:
Supply completion handler closure to call when all the requests are done.
Use DispatchGroup to keep track of when all the requests are done.
Supply a notify closure to your DispatchGroup which will be called when all the group.enter() calls are offset by their respective group.leave() calls.
A more subtle observation is that you should refrain from updating properties from within the responseJSON block. Within your asynchronous code, you really want to limit your interaction to local variables if at all possible. Pass the result back in the completion closure (and the caller can update the model and the UI as it sees fit).

using dispatch group in multi for loop with urlsession tasks

I have using a dispatch group wait() that block my a for loop from completing the code until a set of urlsession tasks (in another loop with completion handler) to be completed before appending new element to my array
the current code will finish the first loop before the second loop of urlClass.selectfoodURL is completed
I want to append the array in meal history after my urlfood for loop is completed
on of the problem in my approach of using dispatch groups is the wait(), when my select food is called the urlsession stuck and doesn’t complete with group.wait
func userSnackHistoryArray() {
let group = DispatchGroup()
let Arrays // array of dictionary
for array in Arrays {
var generateMeal = MealDetails() // struct type
do {
let aa = try JSONDecoder().decode(userSnack.self, from: array)
generateMeal.names = convertToJsonFile.type
for name in generateMeal.names!{
group.enter()
urlClass.selectfoodURL(foodName: name){ success in
generateMeal.units!.append(allVariables.selectedUnit)
group.leave()
}
}
// my select food is called but the urlsession stuck and doesnt complete with group.wait is active
// group.wait()
mealHistory.append(generateMeal)
} catch { }
}
group.notify(queue: .main){
print("complete")
}
}
I have shortened my code to focus on the problem ,, I can split my code into two functions and solve the problem , but I want to use only one function
any suggestions or ideas ?
Rather than waiting, you should just create a local array of values to be added, and then add them when it’s done:
func retrieveSnacks() {
var snacksToAdd: [Snack] = []
let group = DispatchGroup()
...
for url in urls {
group.enter()
fetchSnack(with: url) { result in
dispatchPrecondition(condition: .onQueue(.main)) // note, I’m assuming that this closure is running on the main queue; if not, dispatch this appending of snacks (and `leave` call) to the main queue
if case .success(let snack) = result {
snacksToAdd.append(snack)
}
group.leave()
}
}
// when all the `leave` calls are called, only then append the results
group.notify(queue: .main) {
self.snacks += snacksToAdd
// trigger UI update, or whatever, here
}
}
Note, the above does not assure that the objects are added in the original order. If you need that, you can use a dictionary to build the temporary results and then append the results in sorted order:
func retrieveSnacks() {
var snacksToAdd: [URL: Snack] = [:]
let group = DispatchGroup()
...
for url in urls {
group.enter()
fetchSnack(with: url) { result in
if case .success(let snack) = result {
snacksToAdd[url] = snack
}
group.leave()
}
}
group.notify(queue: .main) {
let sortedSnacks = urls.compactMap { snacksToAdd[$0] }
self.snacks += sortedSnacks
// trigger UI update, or whatever, here
}
}
Finally, I might suggest adopting a completion handler pattern:
func retrieveSnacks(completion: #escaping ([Snack]) -> Void) {
var snacksToAdd: [URL: Snack] = [:]
let group = DispatchGroup()
...
for url in urls {
group.enter()
fetchSnack(with: url) { result in
if case .success(let snack) = result {
snacksToAdd[url] = snack
}
group.leave()
}
}
group.notify(queue: .main) {
let sortedSnacks = urls.compactMap { snacksToAdd[$0] }
completion(sortedSnacks)
}
}
retrieveSnacks { addedSnacks in
self.snacks += addedSnacks
// update UI here
}
This pattern ensures that you don’t entangle your network-related code with your UI code.
I apologize that the above is somewhat refactored from your code snippet, but there wasn’t enough there for me to illustrate what precisely it would look like. But hopefully the above illustrates the pattern and you can see how you’d apply it to your code base. So, don’t get lost in the details, but focus on the basic pattern of building records to be added in a local variable and only update the final results in the .notify block.
FWIW, this is the method signature for the method that the above snippets are using to asynchronously fetch the objects in question.
func fetchSnack(with url: URL, completion: #escaping (Result<Snack, Error>) -> Void) {
...
// if async fetch not successful
DispatchQueue.main.async {
completion(.failure(error))
}
// if successful
DispatchQueue.main.async {
completion(.success(snack))
}
}

Nested async calls in Swift

I'm kind of new to programming in general, so I have this maybe simple question. Actually, writing helps me to identify the problem faster.
Anyway, I have an app with multiple asynchronous calls, they are nested like this:
InstagramUnoficialAPI.shared.getUserId(from: username, success: { (userId) in
InstagramUnoficialAPI.shared.fetchRecentMedia(from: userId, success: { (data) in
InstagramUnoficialAPI.shared.parseMediaJSON(from: data, success: { (media) in
guard let items = media.items else { return }
self.sortMediaToCategories(media: items, success: {
print("success")
// Error Handlers
Looks horrible, but that's not the point. I will investigate the Promise Kit once I get this working.
I need the sortMediaToCategories to wait for completion and then reload my collection view. However, in the sortMediaToCategories I have another nested function, which is async too and has a for in loop.
func sortMediaToCategories(media items: [StoryData.Items],
success: #escaping (() -> Swift.Void),
failure: #escaping (() -> Swift.Void)) {
let group = DispatchGroup()
group.enter()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.notify(queue: .global(), execute: {
self.collectionView.reloadData()
group.leave()
}) },
failure: { print("error") })
//....
I can't afford the collection view to reload every time obviously, so I need to wait for loop to finish and then reload.
I'm trying to use Dispatch Groups, but struggling with it. Could you please help me with this? Any simple examples and any advice will be very appreciated.
The problem you face is a common one: having multiple asynchronous tasks and wait until all are completed.
There are a few solutions. The most simple one is utilising DispatchGroup:
func loadUrls(urls: [URL], completion: #escaping ()->()) {
let grp = DispatchGroup()
urls.forEach { (url) in
grp.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
// handle error
// handle response
grp.leave()
}.resume()
}
grp.notify(queue: DispatchQueue.main) {
completion()
}
}
The function loadUrls is asynchronous and expects an array of URLs as input and a completion handler that will be called when all tasks have been completed. This will be accomplished with the DispatchGroup as demonstrated.
The key is, to ensure that grp.enter() will be called before invoking a task and grp.leave is called when the task has been completed. enter and leave shall be balanced.
grp.notify finally registers a closure which will be called on the specified dispatch queue (here: main) when the DispatchGroup grp balances out (that is, its internal counter reaches zero).
There are a few caveats with this solution, though:
All tasks will be started nearly at the same time and run concurrently
Reporting the final result of all tasks via the completion handler is not shown here. Its implementation will require proper synchronisation.
For all of these caveats there are nice solutions which should be implemented utilising suitable third party libraries. For example, you can submit the tasks to some sort of "executer" which controls how many tasks run concurrently (match like OperationQueue and async Operations).
Many of the "Promise" or "Future" libraries simplify error handling and also help you to solve such problems with just one function call.
You can reloadData when the last item calls the success block in this way.
let lastItemIndex = items.count - 1
for(index, item) in items.enumerated() {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: {
if index == lastItemIndex {
DispatchQueue.global().async {
self.collectionView.reloadData()
}
}
},
failure: { print("error") })
}
You have to move the group.enter() call inside your loop. Calls to enter and leave have to be balanced. If your callbacks of the mediaToStorageDistribution function for success and failure are exclusive you also need to leave the group on failure. When all blocks that called enter leave the group notify will be called. And you probably want to replace the return in your guard statement with a break, to just skip items with missing URLs. Right now you are returning from the whole sortMediaToCatgories function.
func sortMediaToCategories(media items: [StoryData.Items], success: #escaping (() -> Void), failure: #escaping (() -> Void)) {
let group = DispatchGroup()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else { break }
group.enter()
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.leave() },
failure: {
print("error")
group.leave()
})
}
}
group.notify(queue: .main) {
self.collectionView.reloadData()
}
}

Dealing with multiple completion handlers

I'm trying to coordinate several completion handlers for each element in an array.
The code is essentially this:
var results = [String:Int]()
func requestData(for identifiers: [String])
{
identifiers.forEach
{ identifier in
service.request(identifier, completion: { (result) in
result[identifier] = result
})
}
// Execute after all the completion handlers finish
print(result)
}
So each element in the Array is sent through a service with a completion handler, and all the results are stored in an array. Once all of these handlers complete, I wish to execute some code.
I attempted to do this with DispatchQueue
var results = [String:Int]()
func requestData(for identifiers: [String])
{
let queue = DispatchQueue.init(label: "queue")
identifiers.forEach
{ identifier in
service.request(identifier, completion: { (result) in
queue.sync
{
result[identifier] = result
}
})
}
// Execute after all the completion handlers finish
queue.sync
{
print(result)
}
}
but the print call is still being executed first, with an empty Dictionary
If I understand what are you are trying to do correctly, you probably want to use a DispatchGroup
Here is an example:
let group = DispatchGroup()
var letters = ["a", "b", "c"]
for letter in letters {
group.enter()
Server.doSomething(completion: { [weak self] (result) in
print("Letter is: \(letter)")
group.leave()
})
}
group.notify(queue: .main) {
print("- done")
}
This will print something like:
b
c
a
// ^ in some order
- done
First, take note that your service.request(...) is processed in asynchronous mode. Another problem is you want to finish all the service request in that loop.
My suggestion is create the function with completion handler and add a counter on each loop done. Your function will be similarly as below.
var results = [String:Int]()
func requestData(for identifiers: [String], callback:#escaping (Bool) -> Void)
{
var counter = 0
var maxItem = identifiers.count
identifiers.forEach
{ identifier in
service.request(identifier, completion: { (result) in
result[identifier] = result
counter += 1
if counter == maxItem {
callback(true) // update completion handler to say all loops request are done
}
// if not, continue the other request
})
}
}
This is how another part of your code will call the function and wait for callback
requestData(for identifiers:yourArrays) { (complete) in
if complete {
print(results)
}
}
Don't forget to manage if errors happened.