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

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.
}

Related

How to call dataTask method several times with a counter?

I'm currently developing an application using SwiftUI.
I want to call dataTask method several times with while method, a flag, and a counter.
But my code doesn't work...
How could solve this problem?
Here is my code:
func makeCallWithCounter(){
var counter = 0
var flag = false
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.sample = try JSONDecoder().decode([Sample].self, from: responseData)
counter += 1
if counter > 4 {
flag = true
}
}catch{
print("Error: did not decode")
return
}
}
}
while flag == false {
task.resume()
}
}
UPDATED
func makeCallWithCounter(){
var day = 1
var date = "2020-22-\(day)"
var totalTemperature = 0
var counter = 0
var flag = false
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/?date=\(date)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.sample = try JSONDecoder().decode([Sample].self, from: responseData)
day += 1
totalTemperature += self.sample.temperature
if day > 4 {
flag = true
}
}catch{
print("Error: did not decode")
return
}
}
}
while flag == false {
task.resume()
}
print(totalTemperature)
}
Xcode:Version 12.0.1
As I wrote in the comments you need a loop and DispatchGroup. On the other hand you don't need flag and counter and actually not even the URLRequest
I removed the redundant code and there is still a serious error: The line
totalTemperature += sample.temperature
cannot work if sample is an array. The question contains not enough information to be able to fix that.
func makeCallWithCounter() {
var totalTemperature = 0
let group = DispatchGroup()
for day in 1...4 {
// Set up the URL request
let endpoint = "https://sample.com/api/info/?date=2020-22-\(day)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
continue
}
// make the request
group.enter()
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
defer { group.leave() }
if let error = error { print(error); return }
// parse the result as JSON, since that's what the API provides
do {
let sample = try JSONDecoder().decode([Sample].self, from: data!)
totalTemperature += sample.temperature
} catch {
print(error)
}
}
task.resume()
}
group.notify(queue: .main) {
print(totalTemperature)
}
}

Completion Handler True before completed

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)
}
}

Swift 3 - Function Inside DispatchQueue

I called a function inside DispatchQueue.main.async. Here's my code:
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
for i in 0 ... (Global.selectedIcons.count - 1) {
if self.albumorphoto == 1 {
if i == 0 {
self.detector = 1
self.uploadPhoto() //here
}
else {
self.detector = 2
self.uploadPhoto() //here
}
}
else {
self.uploadPhoto() //here
}
}
group.leave()
}
group.notify(queue: .main) {
print("done")
}
}
func uploadPhoto(){
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let params = param
request.httpBody = params.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("error=\(error!)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response!)")
}
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(responseString!)")
if self.detector == 1 {
self.album = self.parseJsonData(data: data)
}
}
task.resume()
}
func parseJsonData(data: Data) -> [AnyObject] {
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
let jsonalbum = jsonResult!["data"] as? [AnyObject]
for jsonAlbum in jsonalbum! {
self.folderID = jsonAlbum["id"] as! String
}
} catch {
print(error)
}
return album
}
I wish to make it wait until all the tasks in DispathcQueue finish. It works but the problem is my function uploadPhoto(). It can't wait until uploadPhoto() finish doing its task. Any idea to solve this? Thanks!
Using a DispatchGroup is the right choice here, but you have to enter and leave for each asynchronous task:
let group = DispatchGroup()
photos.forEach { photo in
group.enter()
// create the request for the photo
URLSession.shared.dataTask(with: request) { data, response, error in
group.leave()
// handle the response
}.resume()
}
group.notify(queue: .main) {
print("All photos uploaded.")
}
You don't need a DispatchQueue.async() call because URLSession.shared.dataTask is already asynchronous.
In my code i assumed that you want to model your objects as Photo and replace Global.selectedIcons.count with a photos array:
class Photo {
let isAlbum: Bool
let isDefector: Bool
let imageData: Data
}
I'd recommend you take a look at Alamofire and SwiftyJSON to further improve your code. These are popular libraries that make dealing with network requests a lot easier. With them you can reduce almost the entire uploadPhoto()/parseJsonData() functions to something like this:
Alamofire.upload(photo.imageData, to: url).responseSwiftyJSON { json in
json["data"].array?.compactMap{ $0["id"].string }.forEach {
self.folderID = $0
}
}
This makes your code more stable because it removes all forced unwrapping. Alamofire also provides you with features like upload progress and resuming & cancelling of requests.

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

a function does internet fetching and return a value

I know the following piece of code is wrong, but I want to show my intent.
I want to write a method that will be called by multiple times. and this fetching method will tell me if it is successfully reached.
func fetch(url: String) -> Bool? {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
var bool: Bool? = nil
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
bool = true
} else {
print("something really wrong")
bool = false
}
}
}).resume()
}
return bool
}
if let bool = fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD"), bool == true {
// if it is true, I can go for next step.
}
Making the UI wait on completion of some API call is not recommended. The app will have no control over how long that API call will take. Situations with bad network connectivity can take several seconds to respond.
You can handle a situation like this is to use a completion handler.
func fetch(url: String, completion: #escaping (_ success: Bool) -> Void) {
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
let url = URL(string: url)
if let url = url {
defaultSession.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
print(error)
return
}
if let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let data = data {
// handle the data.
completion(true)
} else {
print("something really wrong")
completion(false)
}
}).resume()
}
}
func testFetch () {
fetch(url: "https://www.google.com.hk/webhp?hl=en&sa=X&ved=0ahUKEwimubK7r-HVAhVFmZQKHazMAMMQPAgD") { (success) in
// if it is true, I can go for next step.
DispatchQueue.main.async {
if success {
// it was good
}
else {
// not good
}
}
}
}