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()
}
}
Related
Im trying to implement a stock API but get an error with ".append":
No exact matches in call to instance method 'append'.
I'm not familiar with it and need help to solve this problem
final class StockQuoteManager: QuoteManagerProtocol, ObservableObject {
#Published var quotes: [Quote] = []
func download(stocks: [String], completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.dwonloadQueue")
let downloadGroup = DispatchGroup()
stocks.forEach { (stock) in
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) // <-- ERROR
downloadGroup.leave()
}
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
}
This question already has answers here:
How do i return coordinates after forward geocoding?
(3 answers)
block until reverseGeocode has returned
(1 answer)
Closed 1 year ago.
I am new with swift. For my project I need to use google geocoding and to put the result in a text. For the user interface I am using swiftUI. I tried to do the same with Completition Handler but that didn't work. Below I have the code done with DispatchQueue and DispatchGroup but the whole application freezes when I try to use this func. Please help me with this. The code for UI is just a Text calling the func.
func reverseGeocoding(lat: Double, lng: Double) -> String{
var place:String?
let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(lat),\(lng)&key=KEY"
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .default).async {
AF.request(url).responseJSON{ response in
// group.leave()
guard let data = response.data else {
return
}
do {
let jsonData = try JSON(data: data)
let result = jsonData["results"].arrayValue
for result in result {
let address_components = result["types"].arrayValue
for component in address_components {
if(component == "locality"){
place = result["formatted_address"].stringValue
}
}
}
} catch let error {
print(error.localizedDescription)
}
}
}
group.wait()
return place ?? ""
}
continuation for #vadian answer
As mentioned in the above publisher depends on context .This will give you a rough idea.This is from what I understood..
// Replace String with [String] if you want to add multiple locations at once based on it Publisher.send() accepts [String] instead of String
var LocationPublisher = PassthroughSubject<String,Never>()
class Subscriber :ObservableObject {
#Published var currentLocation :[String] = Array<String>()
private var cancellebels = Set< AnyCancellable>()
func createSubscriber(){
let subscriber = LocationPublisher.handleEvents(
receiveSubscription: {subscription in
print("New subscription \(subscription)")},
receiveOutput: {output in
print("New Output \(output)")
},
receiveCancel: {
print("Subscription Canceled")
})
.receive(on: RunLoop.main)
// if you replace String with [String],TypeOf(value) becomes [String]
.sink{value in
print("Subscriber recieved value \(value)")
self.currentLocation.append(value)
// use self.currenLocation.append(contentsOf:value) instead
}
.store(in: &cancellebels)
}
init() {
createSubscriber()
}
}
And inside this contentView
struct ContentView: View {
#ObservedObject var locationObject:Subscriber = Subscriber()
var body: some View {
VStack{
List{
locationObject.currentLocation.forEach{ location in
Text(location)
}
}
}
}
}
and from the above answer inside the success completion handler use
LocationPublisher.send(location)
instead of print statement
it will be notified to subscribers and locationObject.currentLocation will be updated
Its just one way to do it and most basic way.
You need a completion handler like this, it returns also all errors in the Result type
enum GeoError : Error {
case locationNoFound
}
func reverseGeocoding(lat: Double, lng: Double, completion: #escaping (Result<String,Error>) -> Void) {
let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(lat),\(lng)&key=KEY"
DispatchQueue.global(qos: .default).async {
AF.request(url).responseData { response in
switch response.result {
case .success(let data):
do {
let jsonData = try JSON(data: data)
let result = jsonData["results"].arrayValue
for result in result {
let addressComponents = result["types"].arrayValue
for component in addressComponents {
if component == "locality" {
completion(.success(result["formatted_address"].stringValue))
}
}
}
completion(.failure(GeoError.locationNoFound))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
And use it
reverseGeocoding(lat: 45.0, lng: 45.0) { result in
switch result {
case .success(let location): print(location)
case .failure(let error): print(error)
}
}
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 this code to get a list of ids and names that are parsed from a JSON through an iteration of calls.
Problem is I don't know how to get notified, a simples print("finished"), would do. I tried to use print command after the 'for' loop but it also iterates.
Anyone with any idea?
Here's the code:
override func viewDidLoad() {
super.viewDidLoad()
//Manager
let manager = SessionManager.default.startRequestsImmediately = false
//País
let paisRequest = Alamofire.request(self.cadastro_pais_url, method: .post, parameters: self.cadastro_pais_params).responseString { response in
do { } catch { print("error") }
}
for i in 0...2000 {
DispatchQueue.main.async {
let patrocinadorRequest = Alamofire.request(self.buscaPatrocinador, method: .post, parameters: ["patrocinador":"\(i)"]).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
if !(swiftyJsonVar["integracao"] == JSON.null){
print("\(swiftyJsonVar["integracao"]),\(swiftyJsonVar["nome"]),")
} else {}
} else {
print("Error")
}
}
//Requests Chain
let chain = RequestChain(requests: [paisRequest, patrocinadorRequest])
chain.start { (done, error) in
}
}
}
}
The network request should not be done on the main thread, but instead on the background one, sync or async. The main thread is reserved only for the UI stuff, except if you want to force blocking the User interface.
You can use Dispatch Group and DispatchQueue to organise you code and notification after completion. The same result could be achieved with the Semaphore...
Sample:
let dispatchGroup = DispatchGroup()
// change the quality of service based on your needs
let queue = DispatchQueue(label: "com.stackoverflow", qos: .background)
for i in 0...2000 {
dispatchGroup.enter()
// Perform on background thread, async
queue.async {
Alamofire.request { response in
dispatchGroup.leave()
// ...
}
}
}
dispatchGroup.notify(queue: .main, execute: {
print("DONE WITH ALL REQUESTS")
})
Hope it helps.
My app (Swift 5) sends files to a server, using an async completion handler inside a for loop and i.a. a semaphore to ensure that only a single file is sent at the same time.
If the upload fails or if there's an exception, I want to break the loop to display an error message.
My code:
let group = DispatchGroup()
let queue = DispatchQueue(label: "someLabel")
let sema = DispatchSemaphore(value: 0)
queue.async {
for (i,item) in myArray.enumerated() {
group.enter()
do {
let data = try Data(contentsOf: item.url)
ftpProvider.uploadData(folder: "", filename: item.filename, data: data, multipleFiles: true, completion: { (success, error) in
if success {
print("Upload successful!")
} else {
print("Upload failed!")
//TODO: Break here!
}
group.leave()
sema.signal()
})
sema.wait()
} catch {
print("Error: \(error.localizedDescription)")
//TODO: Break here!
}
}
}
group.notify(queue: queue) {
DispatchQueue.main.async {
print("Done!")
}
}
Adding a break gives me an error message:
Unlabeled 'break' is only allowed inside a loop or switch, a labeled
break is required to exit an if or do
Adding a label to the loop (myLoop: for (i,s) in myArray.enumerated()) doesn't work either:
Use of unresolved label 'myLoop'
break self.myLoop fails too.
Adding a print right before group.enter() proves that the loop isn't simply finishing before the upload of the first file is done, instead the text is printed right before "Upload successful"/"Upload failed" is (as it's supposed to). Because of this breaking should be possible:
How do I break the loop, so I can display an error dialog from within group.notify?
A simple solution without using recursion: Add a Bool to check if the loop should break, then break it outside the completion handler:
let group = DispatchGroup()
let queue = DispatchQueue(label: "someLabel")
let sema = DispatchSemaphore(value: 0)
queue.async {
var everythingOkay:Bool = true
for (i,item) in myArray.enumerated() {
//print("Loop iteration: \(i)")
if everythingOkay {
group.enter()
do {
let data = try Data(contentsOf: item.url)
ftpProvider.uploadData(folder: "", filename: item.filename, data: data, multipleFiles: true, completion: { (success, error) in
if success {
print("Upload successful!")
everythingOkay = true
} else {
print("Upload failed!")
everythingOkay = false
}
group.leave()
sema.signal()
})
sema.wait()
} catch {
print("Error: \(error.localizedDescription)")
everythingOkay = false
}
} else {
break
}
}
}
group.notify(queue: queue) {
DispatchQueue.main.async {
print("Done!")
}
}
Usually using a Bool like this wouldn't work because the loop would finish before the first file is even uploaded.
This is where the DispatchGroup and DispatchSemaphore come into play: They ensure that the next loop iteration isn't started until the previous has finished, which means that the files are going to be uploaded in the order they are listed in myArray (this approach was suggested here).
This can be tested with the print in the above code, which is then going to be printed right before "Upload successful!"/"Upload failed!" for every iteration, e.g.:
Loop iteration: 0
Upload successful
Loop iteration: 1
Upload successful
Loop iteration: 2
Upload failed
Done!
My suggested approach is based on AsynchronousOperation provided in the accepted answer of this question.
Create the class, copy the code and create also a subclass of AsynchronousOperation including your asynchronous task and a completion handler
class FTPOperation: AsynchronousOperation {
var completion : ((Result<Bool,Error>) -> Void)?
let item : Item // replace Item with your custom class
init(item : Item) {
self.item = item
}
override func main() {
do {
let data = try Data(contentsOf: item.url)
ftpProvider.uploadData(folder: "", filename: item.filename, data: data, multipleFiles: true) { (success, error) in
if success {
completion?(.success(true))
} else {
completion?(.failure(error))
}
self.finish()
}
} catch {
completion?(.failure(error))
self.finish()
}
}
}
In the controller add a serial operation queue
let operationQueue : OperationQueue = {
let queue = OperationQueue()
queue.name = "FTPQueue"
queue.maxConcurrentOperationCount = 1
return queue
}()
and run the operations. If an error is returned cancel all pending operations
for item in myArray {
let operation = FTPOperation(item: item)
operation.completion = { result in
switch result {
case .success(_) : print("OK", item.filename)
case .failure(let error) :
print(error)
self.operationQueue.cancelAllOperations()
}
}
operationQueue.addOperation(operation)
}
Add a print line in the finish() method of AsynchronousOperation to prove it