I am trying to use a completion handler in a for loop. The problem is that it will keep running the loop before the completion handler returns since it is an async call. Attached is my code. Do I need to use GCD? I am new (obviously)to swift/ios. Any advice would be much appreciated. Bob
for srcTerm in sFields { //search using all search fields
multiQuery (searchTerm: srcTerm) {
if srResult.count < self.lastValue {
self.lastValue = srResult.count
self.lastSearch = srcTerm
}
}
// Do more stuff
}
func multiQuery (searchTerm: String, completion: #escaping ([PFObject]) -> ()) {
var exArray = [PFObject] ()
let query = PFQuery(className: "searchLinks")
do {
query.whereKey("searchTerms", equalTo: searchTerm)
query.findObjectsInBackground (block: { (objects, error)-> Void in
if let error = error {
print("Error Generated: ",error)
return
}
if let objects = objects {
// do stuff
}
completion(self.srResult)
})
}
} // end of function
You could use DispatchGroups, here's an example (taken from https://medium.com/#wilson.balderrama/how-to-use-dispatchgroup-gdc-with-swift-3-35455b9c27e7. Similar to GCD with nested Parse Queries but updated to Swift 3 API):
// Just a sample function to simulate async calls
func run(after seconds: Int, closure: #escaping () -> Void) {
let queue = DispatchQueue.global(qos: .background)
queue.asyncAfter(deadline: .now() + .seconds(seconds)) {
closure()
}
}
let group = DispatchGroup()
group.enter()
run(after: 6) {
print("Hello after 6 seconds")
group.leave()
}
group.enter()
run(after: 3) {
print("Hello after 3 seconds")
group.leave()
}
group.enter()
run(after: 1) {
print("Hello after 1 second")
group.leave()
}
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("All async calls were run!")
}
Using your code:
let group = DispatchGroup()
for srcTerm in sFields { //search using all search fields
group.enter()
multiQuery (searchTerm: srcTerm) {
if srResult.count < self.lastValue {
self.lastValue = srResult.count
self.lastSearch = srcTerm
}
group.leave()
}
}
group.notify(queue: DispatchQueue.global(qos: .background)) {
// Do something after all async calls are done
}
Related
I'm trying to fetch data and update core data based on the new updated API-Data.
I have this download function:
func download1(stock: String, completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.downloadQueue")
let downloadGroup = DispatchGroup()
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { (result) in
switch result {
case .failure(let err):
print(err)
downloadQueue.async {
downloadGroup.leave()
}
case .success(let resp):
downloadQueue.async {
internalQuotes.append(resp.quote)
downloadGroup.leave()
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
On the ContentView I try to implement an update function:
func updateAPI() {
for stock in depot.aktienKatArray {
download.download1(stock: stock.aKat_symbol ?? "") { _ in
//
}
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
PersistenceController.shared.saveContext()
}
My problem is that the for loop in the update function should only go on if the first part (download.download1) is finished with downloading the data from the API.
Don't wait! Never wait!
DispatchGroup is a good choice – however nowadays I highly recommend Swift Concurrency – but it's at the wrong place.
.enter() must be called inside the loop before the asynchronous task starts
.leave() must be called exactly once inside the completion handler of the asynchronous task (ensured by a defer statement)
I know this code won't work most likely, but I merged the two functions to the correct DispatchGroup workflow. I removed the custom queue because the NetworkManager is supposed to do its work on a custom background queue
func updateAPI() {
var internalQuotes = [Quote]()
let downloadGroup = DispatchGroup()
for stock in depot.aktienKatArray {
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { result in
defer { downloadGroup.leave() }
switch result {
case .failure(let err):
print(err)
case .success(let resp):
internalQuotes.append(resp.quote)
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
}
}
downloadGroup.notify(queue: .main) {
self.quotes.append(contentsOf: internalQuotes)
PersistenceController.shared.saveContext()
}
}
I understand that the Firebase getDocument call is Async, so I'm trying to figure out how to essentially wait until the call finishes executing, and then move on to doing other stuff.
I have tried making use of DispatchGroup() and entering/leaving the group, but I can't seem to get it to work correctly. I have something like the following:
let myGroup = DispatchGroup()
let usersRef = self.db.collection("Users").document("Users").collection("Users")
if self.testCondition == false {
self.errorMessage = "error"
} else{
usersRef.getDocuments {(snap, err) in
myGroup.enter()
//basically getting every username
for document in snap!.documents{
let user = document["username"] as! String
let userRef = usersRef.document(user)
userRef.getDocument { (snapshot, err) in
if err != nil {
print(err)
} else {
let sample = snapshot!["sample"] as! String
if sample == 'bad' {
self.errorMessage = "error"
}
}
}
}
myGroup.leave()
}
print("what4")
//I would like it so that I can execute everything in a code block like this
//after the async call finishes
myGroup.notify(queue: .main) {
print("Finished all requests.")
//THEN DO MORE STUFF
}
}
How can I modify the placement myGroup.enter() and myGroup.leave() in this so that, after the Firebase call has finished, I can continue executing code?
Thanks!
This explains the DispatchGroup() a little bit.
You just have one litte mistake in your code then it should be working.
Make sure to enter() the group outside of the Firebase getDocuments() call. As this already makes the request and takes time thus the process will continue.
This little simple example should help you understand it:
func dispatchGroupExample() {
// Initialize the DispatchGroup
let group = DispatchGroup()
print("starting")
// Enter the group outside of the getDocuments call
group.enter()
let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in
if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
}
// leave the group when done
group.leave()
}
// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
}
}
When waiting for multiple asynchronous calls use completing in the asynchronous function which you let return as soon as you leave the group. Full eg. below:
class Test {
init() {
self.twoNestedAsync()
}
func twoNestedAsync() {
let group = DispatchGroup() // Init DispatchGroup
// First Enter
group.enter()
print("calling first asynch")
self.dispatchGroupExample() { isSucceeded in
// Only leave when dispatchGroup returns the escaping bool
if isSucceeded {
group.leave()
} else {
// returned false
group.leave()
}
}
// Enter second
group.enter()
print("calling second asynch")
self.waitAndReturn(){ isSucceeded in
// Only return once the escaping bool comes back
if isSucceeded {
group.leave()
} else {
//returned false
group.leave()
}
}
group.notify(queue: .main) {
print("all asynch done")
}
}
// Now added escaping bool which gets returned when done
func dispatchGroupExample(completing: #escaping (Bool) -> Void) {
// Initialize the DispatchGroup
let group = DispatchGroup()
print("starting")
// Enter the group outside of the getDocuments call
group.enter()
let db = Firestore.firestore()
let docRef = db.collection("test")
docRef.getDocuments { (snapshots, error) in
if let documents = snapshots?.documents {
for doc in documents {
print(doc["name"])
}
// leave the group when succesful and done
group.leave()
}
if let error = error {
// make sure to handle this
completing(false)
group.leave()
}
}
// Continue in here when done above
group.notify(queue: DispatchQueue.global(qos: .background)) {
print("all names returned, we can continue")
//send escaping bool.
completing(true)
}
}
func waitAndReturn(completing: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
print("Done waiting for 2 seconds")
completing(true)
})
}
}
This gives us the following output:
I have a function which downloads data from firebase:
func downloadData(path: String) -> Data? {
let data: Data?
let storage = Storage.storage()
storage.reference(forURL: path).getData(maxSize: 1024 * 1024) { (result, error) in
if let error = error {
print("\(error)")
} else {
data = result
print("finished")
}
return data // <-- this is called before closue ends
}
but it is not returning the value from storage closure.
I tried using a dispatch group
func downloadData(path: String) -> Data? {
...
let group = DispatchGroup()
group.enter()
storage.reference(forURL: path).getData(maxSize: 1024 * 1024) { (result, error) in
...
...
group.leave()
}
group.notify(queue: .main) {
return data // <-- build time error! the function doesn't return a value
}
}
so I tried a different approach: instead of group.notify() I used:
group.wait()
but it didn't work also. the whole app was frozen and nothing happened
and I tried another one:
let semaphore = DispatchSemaphore(value: 0)
storage.reference(forURL: path).getData(maxSize: 1024 * 1024) { (result, error) in
...
...
semaphore.signal()
}
semaphore.wait()
return data // <-- nothing happens... it is not being executed
Any ideas on how to figure this out?
UPDATE
I tried using the completion option but it still didn't wait until it is downloded.
this is my outerFunction:
var soundData: Data?
var imageData: Data?
downloadData(path: soundPath) { sound in
soundData = sound
downloadData(path: imageData) { image in
imageData = image
}
}
doMoreStuff() // <-- called before completeion blocks executed
You should use completion block for data instead of returning the data, here's how:
func downloadData(path: String, completion: #escaping ((Data?) -> Void)?) {
...
let group = DispatchGroup()
group.enter()
var data: Data?
storage.reference(forURL: path).getData(maxSize: 1024 * 1024) { (result, error) in
...
...
data = dataReceived
group.leave()
}
group.notify(queue: .main) {
completion?(data)
}
}
Use completion instead of returning ->
func downloadData(path: String, _ completion: #escaping (Data?) -> Void) {
let data: Data?
let storage = Storage.storage()
storage.reference(forURL: path).getData(maxSize: 1024 * 1024) { (result, error) in
if let error = error {
print("\(error)")
} else {
data = result
print("finished")
}
completion(data)
}
}
Usage of the downloadData(path:,_)
downloadData(path: "") { data in
// Your data is in here
print("Data is:", data)
}
You can't really do that without some kind of completion closure, because you'd be blocking the thread which calls the function, probably the UI thread.
Another alternative would be to use Apple's new Combine framework and return a Future
import Combine
func downloadData(path: String) -> Future<Data, Error> {
return Future<Data, Error> { promise in
let storage = Storage.storage()
storage.reference(forURL: path).getData(maxSize: 1024 * 1024) { (result, error) in
if let error = error {
promise(.failure(error))
} else {
promise(.success(result))
}
}
}
}
var bag = Set<AnyCancellable>()
downloadData(path: "test")
.subscribe(on: DispatchQueue.main)
.sink(receiveCompletion: { (res) in
<#code#>
}) { (data) in
<#code#>
}
.store(in: &bag)
enter code hereI try lot of thing with the dispatch group but I can't obtain stable result.
I use Alamofire for get data since my server. I've write a function in the Helper Class and I use this function in AppDelegate.swift.
I don't know if I put the dispatch group when I call the function so in AppDelegate or I put the dispatch group only in the function in Helper Class.
func alomofireGet(URL: String, onCompletion:#escaping ((JSON) -> Void)) {
// let group = DispatchGroup()
var contentJSON = JSON()
// group.enter()
Alamofire.request(URL, method: .get).responseJSON() { (reponse) in
if reponse.result.isSuccess {
contentJSON = JSON(reponse.result.value!)
} else {
contentJSON = JSON(reponse.result.error!)
}
// group.leave()
}
// group.notify(queue: .main) {
onCompletion(contentJSON)
}
In App delegate, I write a function who call the function in my class.
func connect() {
let group = DispatchGroup()
let _: Bool = KeychainWrapper.standard.removeObject(forKey: "token")
var token = String()
group.enter()
Helper().alomofireGet(URL: "http://192.168.1.19/app_dev.php/login/app") { contenuJSON in
token = contenuJSON["csrfToken"].stringValue
group.leave()
}
group.notify(queue: .main) {
let _: Bool = KeychainWrapper.standard.set(token, forKey: "token")
let t: String? = KeychainWrapper.standard.string(forKey: "token")
print(t!)
}
}
The problem is the variable "t" is empty.
And when I call the keychainWrapper in app delegate, the keychain is empty also.
PS : I have other task, I've just reduce my code
func alomofireGet(URL: String, onCompletion:#escaping ((JSON) -> Void)) {
// let group = DispatchGroup()
var contentJSON = JSON()
// group.enter()
Alamofire.request(URL, method: .get).responseJSON() { (reponse) in
if reponse.result.isSuccess {
contentJSON = JSON(reponse.result.value!)
} else {
contentJSON = JSON(reponse.result.error!)
}
// group.leave()
}
// group.notify(queue: .main) {// where you call wait()function. This blocks the current thread until the group’s tasks have completed.
onCompletion(contentJSON)
}
I try this but is not the solution. I've remove the function in my Helper. I'have juste this function in the app delegate.
func connect(onCompletion : #escaping (String) -> ()) {
let group = DispatchGroup()
var token = String()
let _: Bool = KeychainWrapper.standard.removeObject(forKey: "token")
group.enter()
Alamofire.request("http://192.168.1.19/app_dev.php/login/app", method: .get).responseJSON() { (reponse) in
if reponse.result.isSuccess {
let contentJSON = JSON(reponse.result.value!)
token = contentJSON["csrfToken"].stringValue
} else {
token = "Invalid Token"
}
group.leave()
}
group.notify(queue : DispatchQueue.global()) {
onCompletion(token)
}
}
when I print token, I have a empty message.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Thread.sleep(forTimeInterval: 1.5)
connect() { token in
print(token)
}
return true
}
How can I get the return value from an asynchronous dispatch block?
I came up with this example code:
if let url = URL(string: "https://google.com/") {
let data: Data? = ***SOME_ASYNC_AWAIT_DISPATCH_GROUP<T>*** { return try? Data(contentsOf: url) }
print("Downloaded Data: \(data)")
}
Goal: Here, I want the async call to produce a result and store it to the data constant so that I can use it.
I am simple using the completion method to do this. Test function download the data from url in background thread and after downloading the completion block run, it returns downloaded data, it may in any formate convert it in your formate.
func UpdateUI(){
test { (data) in
//data is value return by test function
DispatchQueue.main.async {
// Update UI
//do task what you want.
// run on the main queue, after the previous code in outer block
}
}
}
func test (returnCompletion: #escaping (AnyObject) -> () ){
let url = URL(string: "https://google.com/")
DispatchQueue.global(qos: .background).async {
// Background work
let data = try? Data(contentsOf: url!)
// convert the data in you formate. here i am using anyobject.
returnCompletion(data as AnyObject)
}
}
Hope it will help you.
I found a solution.
// REFERENCED TO: https://gist.github.com/kylesluder/478bf8fd8232bc90eabd
struct Await<T> {
fileprivate let group: DispatchGroup
fileprivate let getResult: () -> T
#discardableResult func await() -> T { return getResult() }
}
func async<T>(_ queue: DispatchQueue = DispatchQueue.global() , _ block: #escaping () -> T) -> Await<T> {
let group = DispatchGroup()
var result: T?
group.enter()
queue.async(group: group) { result = block(); group.leave() }
group.wait()
return Await(group: group, getResult: { return result! })
}
Call to like this.
let data = async{ return try? Data(contentsOf: someUrl) }.await()
OR
More simple:
#discardableResult func async<T>(_ block: #escaping () -> T) -> T {
let queue = DispatchQueue.global()
let group = DispatchGroup()
var result: T?
group.enter()
queue.async(group: group) { result = block(); group.leave(); }
group.wait()
return result!
}
Call to like this.
let data = async{ return try? Data(contentsOf: someUrl) }
(And thanks for edit my question, Seaman.)
This should work with a sync block
var result:T? = nil
DispatchQueue.global(qos: .background).sync {
//Do something then assigne to result
}
return result