How to exit a for loop with data from an async function in swift? - swift

How can the function findMediaLimit return the highest i, received from cURL ?
class func findMediaLimit(IgBusinessAccount: String, token: String) {
let igBId = IgBusinessAccount
for i in 1...12 {
guard let encodedUrl = self.buildURLAPIGraph(IgBusinessAccount: igBId, token: token, i: i) else { return }
//Async function that returns different i's in random order
self.cURL(urlT: encodedUrl) { (result) in
print(i)
//return the highest i
}
}
}
I have created this function in order to filter media that have been posted after conversion to a business Instagram account.
that's my cURL function
class func cURL (urlT: String,Completion block: #escaping ((OfficialProfile) -> ())) {
//visuJson (urlT: urlT)
GetJson.loadJson(fromURLString: urlT) { (result) in
switch result {
case .success(let data):
//Parse
do {
let decodedData = try JSONDecoder().decode(OfficialProfile.self, from: data)
if decodedData.username != nil {
block(decodedData)
}
} catch {
print("decode error: ",error)
}
case .failure(let error):
print("loadJson error:", error)
}
}
}
and that is my loadJson func
class func loadJson(fromURLString urlString: String,
completion: #escaping (Result<Data, Error>) -> Void) {
if let url = URL(string: urlString) {
let urlSession = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
completion(.success(data))
}
}
urlSession.resume()
}
}

To see what the issue is with what you're apparently trying to do, let's simulate it in a simpler way. I'll use an asynchronous random number generator. Try this in a playground:
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
func asyncRand(_ completion: #escaping (Int) -> ()) {
let d = Double.random(in: 1...3)
delay(d) {
let i = Int.random(in: 1...100)
completion(i)
}
}
func go() {
for i in 1...12 {
asyncRand() { result in
print(i, result)
}
}
print("finished")
}
go()
The result in a typical run might be:
finished
8 13
1 15
9 9
10 56
7 57
3 87
2 70
11 88
6 82
12 16
4 81
5 46
So there are two issues here, because of the asynchronous nature of the central asyncRand call: the results come back in no order, over time, and the go method itself (the loop) finishes before any results come back, so there is no opportunity to find out which result is the maximum.
To straighten that out, while we are waiting for async/await to appear in Swift (possibly as soon as next week), you can use a dispatch group. Ignoring any threading issues, we might do this:
func go() {
let group = DispatchGroup()
var pairs = [[Int]]()
for i in 1...12 {
group.enter()
asyncRand() { result in
print(i, result)
pairs.append([i,result])
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
print("finished")
if let maximum = pairs.max(by: {$0[1] < $1[1]}) {
print(maximum)
}
}
}
Now the result is (for example):
11 52
8 6
2 1
4 6
12 77
1 88
7 45
9 36
6 25
3 22
10 78
5 33
finished
[1, 88]
So you see we have "waited" until all the results have come back, and we have picked out the pair where the largest value was returned.
So you could do something along those lines. But whether that's a good idea is another question.

Related

Swift PropertyWrapper for waiting on a value to set for the first time not working

I intend to wait for the value is set for the first time and then the get method should return. But I am getting wrong result on the first time. Here is the code
import Foundation
import OSLog
#propertyWrapper
struct WaitTillSet<T> {
private var value: T
private let group: DispatchGroup
init(wrappedValue value: T) {
self.group = DispatchGroup()
self.value = value
group.enter()
}
var wrappedValue: T {
get { getValue() }
set { setValue(newValue: newValue) }
}
mutating func setValue(newValue: T) {
value = newValue
group.leave()
}
func getValue() -> T {
group.wait()
return value
}
}
let logger = Logger()
func testFunc() {
#WaitTillSet var data = 0
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 2) {
logger.info("Setting value to 10")
data = 10
}
logger.info("data = \(data)")
logger.info("dataAgain = \(data)")
}
testFunc()
And here is the output
2022-08-23 10:57:39.867967-0700 Test[68117:4747009] Setting value to 10
2022-08-23 10:57:39.868923-0700 Test[68117:4746644] data = 0
2022-08-23 10:57:39.869045-0700 Test[68117:4746644] dataAgain = 10
Program ended with exit code: 0
I also tried DispatchSemaphore. That gives the same result. Instead of propertyWrapper if I use class in the similar way I see the same problem. There is something fundamentally wrong with this.

How to get value out of URLSession

I keep coming back to the same problem. For other parts of my app I used coredata directly after parsing the API and that works fine. However, now I want to parse a JSON received through an API and just get one value that I use to calculate other values which I then put in Coredata.
Everything works fine and I have set up the URLSessions code as follows:
func fetchData(brand: String, completion: #escaping ((Double) -> Void)) {
let urlString = "\(quoteUrl)\(brand)"
if let url = URL(string: urlString) {
var session = URLRequest(url: url)
session.addValue("application/json", forHTTPHeaderField: "Accept")
session.addValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: session) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(DataModel.self, from: safeData)
let bid = decodedData.quotes.quote.bid
let ask = decodedData.quotes.quote.ask
let itemPrice: Double = (bid + ask)/2
completion(itemPrice)
} catch {
print(error)
}
}
}
task.resume()
}
}
I am using the completionHandler to retrieve the part I need which I use in another file as follows:
func getGainLossNumber(brand: String, quantity: Int, price: Double) -> Double {
var finalPrice = 0.0
APImodel.fetchData(brand: brand) { returnedDouble in
let currentPrice = returnedDouble
if quantity < 0 {
let orderQuantity = quantity * -1
finalPrice = price + (currentPrice*(Double(orderQuantity))*100)
} else {
finalPrice = price - (currentPrice*(Double(quantity))*100)
}
}
return finalPrice
}
finalPrice eventually returns 0.0. If I print currentPrice in the closure I do get the correct result. I used the completion handler in order to retrieve a number from the API because of the issues I was facing but it stil is not doing what I would like to have. The second function should return the value that was calculated using the value I got from the API that I retrieved with the completion handler.
I just can't figure out how to do it.
The problem is that you are calculating finalPrice inside a closure, which is asynchronous. Your getGainLossNumber method however, is synchronous, so it actually returns before your closure is finished calculating finalPrice. Restructure your code so that getGainLossNumber takes a closure as a parameter, and invokes it once finalPrice has been calculated. Something like:
func getGainLossNumber(brand: String, quantity: Int, price: Double, _ completion: #escaping (Double) -> Void) {
APImodel.fetchData(brand: brand) { returnedDouble in
let currentPrice = returnedDouble
let finalPrice: Double
if quantity < 0 {
let orderQuantity = quantity * -1
finalPrice = price + (currentPrice*(Double(orderQuantity))*100)
}
else {
finalPrice = price - (currentPrice*(Double(quantity))*100)
}
completion(finalPrice)
}
}
Also note, that finalPrice does not need to be var as it will be assigned a value only once.
EDIT
Usage:
getGainLossNumber(brand: "brand", quantity: 1, price: 120, { finalPrice in
// You can access/use finalPrice in here.
}

Swift Completion Handler For Loop to be performed once instead of 10 times due to the loop

I I have a loop with a firestore query in it that is repeated 10 times. I need to call the (completion: block) after all the 10 queries completed; Here I have my code so that it performs the (completion: block) per query but this would be too heavy on the server and the user's phone. How can I change below to accomplish what I just described?
static func getSearchedProducts(fetchingNumberToStart: Int, sortedProducts: [Int : [String : Int]], handler: #escaping (_ products: [Product], _ lastFetchedNumber: Int?) -> Void) {
var lastFetchedNumber:Int = 0
var searchedProducts:[Product] = []
let db = Firestore.firestore()
let block : FIRQuerySnapshotBlock = ({ (snap, error) in
guard error == nil, let snapshot = snap else {
debugPrint(error?.localizedDescription)
return
}
var products = snapshot.documents.map { Product(data: $0.data()) }
if !UserService.current.isGuest {
db.collection(DatabaseRef.Users).document(Auth.auth().currentUser?.uid ?? "").collection(DatabaseRef.Cart).getDocuments { (cartSnapshot, error) in
guard error == nil, let cartSnapshot = cartSnapshot else {
return
}
cartSnapshot.documents.forEach { document in
var product = Product(data: document.data())
guard let index = products.firstIndex(of: product) else { return }
let cartCount: Int = document.exists ? document.get(DatabaseRef.cartCount) as? Int ?? 0 : 0
product.cartCount = cartCount
products[index] = product
}
handler(products, lastFetchedNumber)
}
}
else {
handler(products, lastFetchedNumber)
}
})
if lastFetchedNumber == fetchingNumberToStart {
for _ in 0 ..< 10 {
//change the fetching number each time in the loop
lastFetchedNumber = lastFetchedNumber + 1
let productId = sortedProducts[lastFetchedNumber]?.keys.first ?? ""
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
}
}
}
as you can see at the very end I am looping 10 times for this query because of for _ in 0 ..< 10 :
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
So I need to make the completion: block handler to be called only once instead of 10 times here.
Use a DispatchGroup. You can enter the dispatch group each time you call the async code and then leave each time it's done. Then when everything is finished it will call the notify block and you can call your handler. Here's a quick example of what that would look like:
let dispatchGroup = DispatchGroup()
let array = []
for i in array {
dispatchGroup.enter()
somethingAsync() {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
handler()
}

Index out of Range exception when using firebase database and storage to download data

Here is my method to retrieve an array of user and post objects from the database.
func getRecentPost(start timestamp: Int? = nil, limit: UInt, completionHandler: #escaping ([(Post, UserObject)]) -> Void) {
var feedQuery = REF_POSTS.queryOrdered(byChild: "timestamp")
if let latestPostTimestamp = timestamp, latestPostTimestamp > 0 {
feedQuery = feedQuery.queryStarting(atValue: latestPostTimestamp + 1, childKey: "timestamp").queryLimited(toLast: limit)
} else {
feedQuery = feedQuery.queryLimited(toLast: limit)
}
// Call Firebase API to retrieve the latest records
feedQuery.observeSingleEvent(of: .value, with: { (snapshot) in
let items = snapshot.children.allObjects
let myGroup = DispatchGroup()
var results: [(post: Post, user: UserObject)] = []
for (index, item) in (items as! [DataSnapshot]).enumerated() {
myGroup.enter()
Api.Post.observePost(withId: item.key, completion: { (post) in
Api.User.observeUser(withId: post.uid!, completion: { (user) in
results.insert((post, user), at: index) //here is where I get my error -> Array index is out of range
myGroup.leave()
})
})
}
myGroup.notify(queue: .main) {
results.sort(by: {$0.0.timestamp! > $1.0.timestamp! })
completionHandler(results)
}
})
}
Here is the call to the method from my view controller. I am currently using texture UI to help with a faster smoother UI.
var firstFetch = true
func fetchNewBatchWithContext(_ context: ASBatchContext?) {
if firstFetch {
firstFetch = false
isLoadingPost = true
print("Begin First Fetch")
Api.Post.getRecentPost(start: posts.first?.timestamp, limit: 8 ) { (results) in
if results.count > 0 {
results.forEach({ (result) in
posts.append(result.0)
users.append(result.1)
})
}
self.addRowsIntoTableNode(newPhotoCount: results.count)
print("First Batch Fetched")
context?.completeBatchFetching(true)
isLoadingPost = false
print("First Batch", isLoadingPost)
}
} else {
guard !isLoadingPost else {
context?.completeBatchFetching(true)
return
}
isLoadingPost = true
guard let lastPostTimestamp = posts.last?.timestamp else {
isLoadingPost = false
return
}
Api.Post.getOldPost(start: lastPostTimestamp, limit: 9) { (results) in
if results.count == 0 {
return
}
for result in results {
posts.append(result.0)
users.append(result.1)
}
self.addRowsIntoTableNode(newPhotoCount: results.count)
context?.completeBatchFetching(true)
isLoadingPost = false
print("Next Batch", isLoadingPost)
}
}
}
In the first section of code, I have debugged to see if I could figure out what is happening. Currently, firebase is returning the correct number of objects that I have limited my query to (8). But, where I have highlighted the error occurring at, the index jumps when it is about to insert the fifth object, index[3] -> 4th object is in array, to index[7]-> 5th object about to be parsed and inserted, when parsing the 5th object.
So instead of going from index[3] to index[4] it jumps to index[7].
Can someone help me understand what is happening and how to fix it?
The for loop has continued on its thread while the observeUser & observePost callbacks are on other threads. Looking at your code, you can probably just get away with appending the object to the results array instead of inserting. This makes sense because you are sorting after the for loop anyway, so why does the order matter?

Grand Central Dispatch for complex flow?

I have a, b, c, d, e time consuming task functions with completion handler.
There are constraints between them:
Both b & c wait for a to finish
The last task e waits for b & c & d to finish
if there is no task d, I could write code in swift like this (not tested yet)
let group = DispatchGroup()
group.enter()
a() { group.leave() }
group.wait()
group.enter()
b() { group.leave() }
group.enter()
c() { group.leave() }
group.notify(queue: .main) {
e()
}
How to add task d without waiting a to complete?
Edited on 4/30 10:00 (+8)
Code Different said
the easiest approach is to make the download function synchronous and add a warning to its documentation that it should never be called from the main thread.
So I made a version based on it. This way cannot handle the return values from concurrent calls. But it looks really like async/await. So I'm satisfied now. Thank you guys.
the async/await like part is
myQueue.async {
downloadSync("A")
downloadSync("B", isConcurrent: true)
downloadSync("C", isConcurrent: true)
downloadSync("D", 4, isConcurrent: true)
waitConcurrentJobs()
downloadSync("E")
}
And the full code is below.
let myGroup = DispatchGroup()
let myQueue = DispatchQueue(label: "for Sync/Blocking version of async functions")
func waitConcurrentJobs() {
myGroup.wait()
}
// original function (async version, no source code)
func download(_ something: String, _ seconds: UInt32 = 1, completionHandler: #escaping ()->Void = {}) {
print("Downloading \(something)")
DispatchQueue.global().async {
sleep(seconds)
print("\(something) is downloaded")
completionHandler()
}
}
// wrapped function (synced version)
// Warning:
// It blocks current thead !!!
// Do not call it on main thread
func downloadSync(
_ something: String,
_ seconds: UInt32 = 1,
isConcurrent: Bool = false
){
myGroup.enter()
download(something, seconds) { myGroup.leave() }
if !isConcurrent {
myGroup.wait()
}
}
// Now it really looks like ES8 async/await
myQueue.async {
downloadSync("A")
downloadSync("B", isConcurrent: true)
downloadSync("C", isConcurrent: true)
downloadSync("D", 4, isConcurrent: true)
waitConcurrentJobs()
downloadSync("E")
}
results
Edit: the easiest approach is to make the download function synchronous and add a warning to its documentation that it should never be called from the main thread. The pyramid of doom for async function is the reason why coroutines were proposed, by no other than Chris Lattner, Swift's creator. As of April 2018, it's not yet a formal proposal waiting for review so chances are that you won't see it in Swift 5.
An synchronous download function:
// Never call this from main thread
func download(_ something: String, _ seconds: UInt32 = 1, completionHandler: #escaping ()->Void = {}) {
let group = DispatchGroup()
print("Downloading \(something)")
group.enter()
DispatchQueue.global().async {
sleep(seconds)
print("\(something) is downloaded")
completionHandler()
group.leave()
}
group.wait()
}
And NSOperation / NSOperationQueue setup:
let opA = BlockOperation() {
self.download("A")
}
let opB = BlockOperation() {
self.download("B")
}
let opC = BlockOperation() {
self.download("C")
}
let opD = BlockOperation() {
self.download("D", 4)
}
let opE = BlockOperation() {
self.download("E")
}
opB.addDependency(opA)
opC.addDependency(opA)
opE.addDependency(opB)
opE.addDependency(opC)
opE.addDependency(opD)
let operationQueue = OperationQueue()
operationQueue.addOperations([opA, opB, opC, opD, opE], waitUntilFinished: false)
Your original effort seems very close to me. You could make a minor adjustment: make B, C, and D be the group that finishes to trigger E.
A could be another group, but since it's one task, I don't see the point. Trigger B and C when it's done.
Note that unlike some of the example code in your question and other answers, in the code below, D and A can both start right away and run in parallel.
let q = DispatchQueue(label: "my-queue", attributes: .concurrent)
let g = DispatchGroup()
func taskA() { print("A") }
func taskB() { print("B"); g.leave() }
func taskC() { print("C"); g.leave() }
func taskD() { print("D"); g.leave() }
func taskE() { print("E") }
g.enter()
g.enter()
g.enter()
q.async {
taskA()
q.async(execute: taskB)
q.async(execute: taskC)
}
q.async(execute: taskD)
g.notify(queue: q, execute: taskE)
You can use this framework to implement async/await pattern - https://github.com/belozierov/SwiftCoroutine
When you call await it doesn’t block the thread but only suspends coroutine, so you can use it in the main thread as well.
func awaitAPICall(_ url: URL) throws -> String? {
let future = URLSession.shared.dataTaskFuture(for: url)
let data = try future.await().data
return String(data: data, encoding: .utf8)
}
func load(url: URL) {
DispatchQueue.main.startCoroutine {
let result1 = try self.awaitAPICall(url)
let result2 = try self.awaitAPICall2(result1)
let result3 = try self.awaitAPICall3(result2)
print(result3)
}
}
I would like to show an alternative solution using Scala like futures:
let result = funcA().flatMap { resultA in
return [funcB(param: resultA.0),
funcC(param: resultA.1),
funcD()]
.fold(initial: [String]()) { (combined, element) in
return combined + [element]
}
}.flatMap { result in
return funcE(param: result)
}.map { result in
print(result)
}
That's it basically. It handles errors (implicitly) and is thread-safe. No Operation subclasses ;)
Note, that funcD will be called only when A completes successfully. Since funcA() can fail it would make no sense to call it. But the code can be easily adapted to make this possible as well, iff required.
Please compare this to function foo() from my other solution which uses Dispatch Groups and Dispatch Queues.
Below an example of the definitions of the async functions which each passing their result to the next one:
func funcA() -> Future<(String, String)> {
print("Start A")
let promise = Promise<(String, String)>()
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
print("Complete A")
promise.complete(("A1", "A2"))
}
return promise.future
}
func funcB(param: String) -> Future<String> {
print("Start B")
let promise = Promise<String>()
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
print("Complete B")
promise.complete("\(param) -> B")
}
return promise.future
}
func funcC(param: String) -> Future<String> {
print("Start C")
let promise = Promise<String>()
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
print("Complete C")
promise.complete("\(param) -> C")
}
return promise.future
}
func funcD() -> Future<String> {
print("Start D")
let promise = Promise<String>()
DispatchQueue.global().asyncAfter(deadline: .now() + 4) {
print("Complete D")
promise.complete("D")
}
return promise.future
}
func funcE(param: [String]) -> Future<String> {
print("Start E")
let promise = Promise<String>()
DispatchQueue.global().asyncAfter(deadline: .now() + 4) {
print("Complete E")
promise.complete("\(param) -> E")
}
return promise.future
}
Which prints this to the console:
Start A
Complete A
Start B
Start C
Start D
Complete B
Complete C
Complete D
Start E
Complete E
["A1 -> B", "A2 -> C", "D"] -> E
Hint: there are a couple of Future and Promise libraries available.