Completion Handler True before completed - swift

so I have a function that gets a quote and author from an API. I have a completion handler so that i can get the quote and author and then set them to their respective UILabel in the Viewdidload function. But for some reason both the quote and author come up nil. What's going wrong with the handler?
func getJSON(completionHandler: #escaping(CompletionHandler)){
if let quoteURL = URL(string: "http://quotes.rest/qod.json")
{
let session = URLSession.shared
let task = session.dataTask(with: quoteURL)
{ (data, response, error) -> Void in
if data != nil
{
let quoteData = JSON(data: data!)
self.quote = quoteData["contents"]["quotes"][0]["quote"].stringValue
self.author = quoteData["contents"]["quotes"][0]["author"].stringValue
}
}
task.resume()
}
completionHandler(true)
}
Calling the function in the Viewdidload()
self.getJSON(completionHandler: {(success)-> Void in
if(success){
self.quoteLabel.text = "\(self.quote ?? "") - \(self.author ?? "")"
}
})
Swift doesn't allow you to set UILabel text in background processes which is why i cannot do it in getJSON()
Thanks

You need to insert it inside the callback
func getJSON(completionHandler: #escaping(CompletionHandler)){
if let quoteURL = URL(string: "http://quotes.rest/qod.json")
{
let session = URLSession.shared
let task = session.dataTask(with: quoteURL)
{ (data, response, error) -> Void in
if data != nil
{
let quoteData = JSON(data: data!)
self.quote = quoteData["contents"]["quotes"][0]["quote"].stringValue
self.author = quoteData["contents"]["quotes"][0]["author"].stringValue
completionHandler(true) // set it inside the callback
}
else {
completionHandler(false)
}
}
task.resume()
}
else {
completionHandler(false)
}
}

Related

In Swift, how do you loop through a list and hand one item at a time to a function with completion closure?

I'm trying to process a folder with audio files through speech to text recognition on MacOS.
If I just process one file, it works, but if I feed multiple files, only one file works and throws an error for rest.
I thought I could use DispatchGroup, but it still feeds everything at once instead of waiting for each item to be completed.
Could someone help me to understand what I'm doing wrong?
let recognizer = SFSpeechRecognizer()
recognizer?.supportsOnDeviceRecognition = true
let group = DispatchGroup()
let fd = FileManager.default
fd.enumerator(at: url, includingPropertiesForKeys: nil)?.forEach({ (e) in
if let url = e as? URL, url.pathExtension == "wav" || url.pathExtension == "aiff" {
let request = SFSpeechURLRecognitionRequest(url: url)
group.enter()
let task = recognizer?.recognitionTask(with: request) { (result, error) in
print("Transcribing \(url.lastPathComponent)")
guard let result = result else {
print("\(url.lastPathComponent): No message")
group.leave()
return
}
while result.isFinal == false {
sleep(1)
}
print("\(url.lastPathComponent): \(result.bestTranscription.formattedString)")
group.leave()
}
group.wait()
}
}
group.notify(queue: .main) {
print("Done")
}
Update: I tried DispatchQueue, but it transcribes only one file and hangs.
let recognizer = SFSpeechRecognizer()
recognizer?.supportsOnDeviceRecognition = true
let fd = FileManager.default
let q = DispatchQueue(label: "serial q")
fd.enumerator(at: url, includingPropertiesForKeys: nil)?.forEach({ (e) in
if let url = e as? URL, url.pathExtension == "wav" {
let request = SFSpeechURLRecognitionRequest(url: url)
q.sync {
let task = recognizer?.recognitionTask(with: request) { (result, error) in
guard let result = result else {
print("\(url.lastPathComponent): No message")
return
}
if result.isFinal {
print("\(url.lastPathComponent): \(result.bestTranscription.formattedString)")
}
}
}
}
})
print("Done")
This is a async/await solution with a Continuation. It runs sequentially.
let recognizer = SFSpeechRecognizer()
recognizer?.supportsOnDeviceRecognition = true
let fd = FileManager.default
let enumerator = fd.enumerator(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)!
Task {
for case let fileURL as URL in enumerator where ["wav", "aiff"].contains(fileURL.pathExtension) {
do {
try await recognizeText(at: fileURL)
} catch {
print(error)
}
}
}
func recognizeText(at url: URL) async throws {
return try await withCheckedThrowingContinuation { (continuation : CheckedContinuation<Void, Error>) in
let request = SFSpeechURLRecognitionRequest(url: url)
let task = recognizer?.recognitionTask(with: request) { (result, error) in
print("Transcribing \(url.lastPathComponent)")
if let error = error {
continuation.resume(throwing: error)
print("\(url.lastPathComponent): No message")
} else {
print("\(url.lastPathComponent): \(result!.bestTranscription.formattedString)")
if result!.isFinal {
continuation.resume(returning: ())
}
}
}
}
}
If you want your dispatch group to wait for each task to complete before submitting the next, you need to add a `group.wait() inside the loop, after submitting each task.
// Your setup code is unchanged...
fd.enumerator(at: url, includingPropertiesForKeys: nil)?.forEach({ (e) in
if let url = e as? URL, url.pathExtension == "wav" || url.pathExtension == "aiff" {
let request = SFSpeechURLRecognitionRequest(url: url)
group.enter()
let task = recognizer?.recognitionTask(with: request) { (result, error) in
print("Transcribing \(url.lastPathComponent)")
guard let result = result else {
print("\(url.lastPathComponent): No message")
group.leave()
return
}
while result.isFinal == false {
sleep(1)
}
print("\(url.lastPathComponent): \(result.bestTranscription.formattedString)")
group.leave()
}
group.wait() // <---- Add this
}
That should do it.
Note that doing it this way will block the main thread. You should really wrap the code that submits jobs and waits for the last one to finish in a call to a background dispatch queue.
Something like this:
DispatchQueue.global().async {
// Code to loop through and submit tasks, including dispatchGroup logic above.
}

return value from completion handler is not updated in DispatchQueue.main.async block

I am calling a function with completion handler from one class to another class
called class:
class PVClass
{
var avgMonthlyAcKw:Double = 0.0
var jsonString:String!
func estimateMonthlyACkW (areaSqFt:Float, completion: #escaping(Double) -> () ){
var capacityStr:String = ""
let estimatedCapacity = Float(areaSqFt/66.0)
capacityStr = String(format: "%.2f", estimatedCapacity)
// Build some Url string
var urlString:String = "https://developer.nrel.gov/"
urlString.append("&system_capacity=")
urlString.append(capacityStr)
let pvURL = URL(string: urlString)
let dataTask = URLSession.shared.dataTask(with: pvURL!) { data, response, error in
do {
let _ = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
self.jsonString = String(data: data!, encoding: .utf8)!
print("JSON String:\(String(describing: self.jsonString))")
if self.jsonString != nil {
let decoder = JSONDecoder()
let jsonData = try decoder.decode(PVClass.Top.self, from: data!)
// do some parsing here
var totalAcKw: Double = 0.0
let cnt2: Int = (jsonData.Outputs?.ACMonthly.count)!
for i in 0..<(cnt2-1) {
totalAcKw = totalAcKw + (jsonData.Outputs?.ACMonthly[i])!
}
self.avgMonthlyAcKw = Double(totalAcKw)/Double(cnt2)
// prints value
print("updated estimate: ", self.avgMonthlyAcKw)
completion(self.avgMonthlyAcKw)
}
} catch {
print("error: \(error.localizedDescription)")
}
}
dataTask.resume()
}
calling class:
aPVClass.estimateMonthlyACkW(areaSqFt: 100.0, completion: { (monthlyAckW) -> Void in
DispatchQueue.main.async { [weak self] in
guard case self = self else {
return
}
print("monthlyAckW: ", monthlyAckW)
self?.estimatedSolarkWh = Int(monthlyAckW * Double((12)/365 * (self?.numDays)!))
print("estimatedSolarkWh: ", self?.estimatedSolarkWh ?? 0)
guard let val = self?.estimatedSolarkWh else { return }
print("val: ", val)
self?.estimatedSolarkWhLabel.text = String(val)
self?.view.setNeedsDisplay()
}
})
}
monthlyAckW has the right value after completion handler returns. But the assigned value to self?.estimatedSolarkWh is 0, value never gets transferred to the current class scope, UI update fails, even after DispatchQueue.main.async
How to fix this please?
The call of completion is at the wrong place. Move it into the completion closure of the data task after the print line
// prints value
print("updated estimate: ", self.avgMonthlyAcKw)
completion(self.avgMonthlyAcKw)
and delete it after resume
dataTask.resume()
completion(self.avgMonthlyAcKw)
}

Data tasks outside ViewController

I'm gonna start with I'm currently learning swift + iOS so I'm by no means an experienced developer or one for that matter.
My goal is to separate any network calls that are currently done in my view controller to a dedicated class outside of it.
In this view controller i have a IBAction with the following code inside of it:
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
What I want, or I think I want to achieve is something like this:
let apiData = "somehow get it from outside"
Then when apiData has info stored in it, execute this next bit of code:
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
How would I achieve this? Thank you.
You can try
class API {
static func userLoginWith(email:String,password:String,completion:#escaping(_ token:String?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else { completion(nil) ; return }
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(apiData.data?.token)
} catch {
print("Decoder error: ",error")
completion(nil)
}
}.resume()
}
}
Inside the VC
API.userLoginWith(email:<##>,password:<##>) { (token) in
if let token = token {
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
}
}

Swift 4: How to asynchronously use URLSessionDataTask but have the requests be in a timed queue?

Basically I have some JSON data that I wish to retrieve from a bunch of URL's (all from the same host), however I can only request this data roughly every 2 seconds at minimum and only one at a time or I'll be "time banned" from the server. As you'll see below; while URLSession is very quick it also gets me time banned almost instantly when I have around 700 urls to get through.
How would I go about creating a queue in URLSession (if its functionality supports it) and while having it work asynchronously to my main thread; have it work serially on its own thread and only attempt each item in the queue after 2 seconds have past since it finished the previous request?
for url in urls {
get(url: url)
}
func get(url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
print(error.localizedDescription)
}
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
DispatchQueue.main.async {
print("Server Error")
}
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
DispatchQueue.main.async {
self.itemsCount.append(count)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
Recursion solves this best
import Foundation
import PlaygroundSupport
// Let asynchronous code run
PlaygroundPage.current.needsIndefiniteExecution = true
func fetch(urls: [URL]) {
guard urls.count > 0 else {
print("Queue finished")
return
}
var pendingURLs = urls
let currentUrl = pendingURLs.removeFirst()
print("\(pendingURLs.count)")
let session = URLSession.shared
let task = session.dataTask(with: currentUrl, completionHandler: { (data, response, error) in
print("task completed")
if let _ = error {
print("error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("server error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
if response.mimeType == "application/json" {
print("json data parsed")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}else {
print("unknown data")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}
})
//start execution after two seconds
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
print("resume called")
task.resume()
}
}
var urls = [URL]()
for _ in 0..<100 {
if let url = URL(string: "https://google.com") {
urls.append(url)
}
}
fetch(urls:urls)
The easiest way is to perform recursive call:
Imagine you have array with your urls.
In place where you initially perform for loop with, replace it with single call get(url:).
self.get(urls[0])
Then add this line at the and of response closure right after self.itemsCount.append(count):
self.urls.removeFirst()
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
self.get(url: urls[0])
}
Make DispatchQueue to run your code on threads. You don't need to do this work on Main Thread. So,
// make serial queue
let queue = DispatchQueue(label: "getData")
// for delay
func wait(seconds: Double, completion: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + seconds) { completion() }
}
// usage
for url in urls {
wait(seconds: 2.0) {
self.get(url: url) { (itemCount) in
// update UI related to itemCount
}
}
}
By the way, Your get(url: url) function is not that great.
func get(url: URL, completionHandler: #escaping ([Int]) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print(error.localizedDescription)
/* Don't need to use main thread
DispatchQueue.main.async {
print(error.localizedDescription)
}
*/
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("Server Error")
/* Don't need to use main thread
DispatchQueue.main.async {
print("Server Error")
}
*/
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
self.itemsCount.append(count)
// append all data that you need and pass it to completion closure
DispatchQueue.main.async {
completionHandler(self.itemsCount)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
I would recommend you to learn concept of GCD(for thread) and escaping closure(for completion handler).
GCD: https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
Escaping Closure: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID546

Swift: Ensure urlSession.dataTask is completed in my function before passing result

Hello I have this function:
func planAdded(id:Int, user_id:Int) -> Int {
let locationURL = "myurl"
var planResult: Int = 0
let request = URLRequest(url: URL(string: locationURL)!)
let urlSession = URLSession.shared
let task = urlSession.dataTask(with: request, completionHandler:{
(data, response, error) -> Void in
DispatchQueue.main.async {
if let error = error {
print (error)
return
}
if let data = data {
let responseString = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
planResult = responseString!.integerValue
}
}
})
task.resume()
print(planResult)
return planResult
}
What I am trying to do is to ensure that I got the result for planResult in tableView cellforrow at indexpath function.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
case 4:
if (result == 1){
...
} else if (result == 2){
...
} else {
...
}
default:
cell.fieldLabel.text = ""
}
return cell
}
Here is my viewDidLoad function
override func viewDidLoad() {
super.viewDidLoad()
self.result = self.planAdded(1, 2)
}
For some reasons, this keeps returning 0; however, the print line is actually printing correct value. I did some research and I believe this is because of asychonous call of the dataTask. Is there a way I ensure that my function is actually completed and return the value for the indexpath function?
Thanks
The reason is, you are doing it in a wrong way! Because, once you intialize the class the UIViewController lifecycle starts. Once the viewDidLoad() is called it the UITableView is also updated with no data.
Also, you are calling API to get the data, you need to notify UITableViewDataSource to update data and here is how you can do that!
func planAdded(id:Int, user_id:Int) {
let locationURL = "myurl"
var planResult: Int = 0
let request = URLRequest(url: URL(string: locationURL)!)
let urlSession = URLSession.shared
let task = urlSession.dataTask(with: request, completionHandler:{
(data, response, error) -> Void in
DispatchQueue.main.async {
if let error = error {
print (error)
return
}
if let data = data {
let responseString = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
self.result = responseString!.integerValue
self.tableView.reloadData()
}
}
})
task.resume()
}
And you are getting zero value because it's an async method. So get the data you need to use completionCallback.
func planAdded(id:Int, user_id:Int, completion: (result: Int) -> ()) {
let locationURL = "myurl"
var planResult: Int = 0
let request = URLRequest(url: URL(string: locationURL)!)
let urlSession = URLSession.shared
let task = urlSession.dataTask(with: request, completionHandler:{
(data, response, error) -> Void in
DispatchQueue.main.async {
if let error = error {
print (error)
return
}
if let data = data {
let responseString = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
planResult = responseString!.integerValue
completion(planResult)
}
}
})
task.resume()
}
Usage:
override func viewDidLoad() {
super.viewDidLoad()
planAdded(1, 2){(value) in
self.result = value
self.tableView.reloadData()
}
}