Swift - Async/Await not stopping to let task finish - swift

I'm writing a simple program designed to talk to a server, get some information, and display it. However, I'm running into issues with the async code, mainly that the code isn't stopping to allow the server to respond before continuing.
I know I have to be doing something wrong but have no idea what, any help is appreciated.
override func viewDidLoad() {
super.viewDidLoad()
Task{
let accessToken = await getToken()
}
print("Done")
}
private func getToken() async -> String{
let url = URL(string: "https://api.petfinder.com/v2/oauth2/token")
let payload = "grant_type=client_credentials&client_id=GUQj1MdQN3QunoxXz4vdd0DHPlcJC6yuqCLCEXavriJ4W6wTYV&client_secret=7whgSG3ZX6m9Cwfr2vEakOH90fSn3g0isIlae0CC".data(using: .utf8)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
request.httpBody = payload
do{
let (data,_) = try await URLSession.shared.data(for: request)
let APItoken: token = try! JSONDecoder().decode(token.self, from: data)
return APItoken.access_token
}
catch{
return ""
}
}

If I understood the problem, you see "Done" being printed before the getToken() method is completed, right?
The problem is that print("Done") is outside of the Task.
When you call Task, it starts running what's in the closure and it immediately resumes after the closure, while your task is running in parallel, without blocking the main thread.
Place your print() inside the Task closure, right after the getToken() method, and you'll see that it'll be "Done" after you complete your POST request.
Task{
let accessToken = await getToken()
print("Done")
}

Await does not completely block your code, instead the thread that makes the call can be used to do something else, like get more user input, execute other task content that was waiting for a result and now has it. think like closure call backs, everything after the await, will be executed later, probable up to the enclosing task. Print(“done”) is not in your task, so it’s not required to wait for the code in the task execute, the task content is probable executing in another thread, as it doesn’t contain code that has to execute in the main thread.

Related

Cancelling the task of a URLSession.AsyncBytes doesn't seem to work

I want to download a large file, knowing the number of bytes transferred, and be able to cancel the download if necessary.
I know that this can be done having a URLSessionDownloadTask and conforming to the URLSessionDownloadDelegate, but I wanted to achieve it through an async/await mechanism, so I used URLSession.shared.bytes(from: url) and then a for-await-in loop to handle each byte.
The issue comes when trying to cancel the ongoing task, as even though the URLSession.AsyncBytes's Task has been cancelled, the for-await-in loop keeps processing bytes, so I'm assuming that the download is still ongoing.
I've tested it with this piece of code in a playground.
let url = URL(string: "https://example.com/large_file.zip")!
let (asyncBytes, _) = try await URLSession.shared.bytes(from: url)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
asyncBytes.task.cancel()
}
var data = Data()
for try await byte in asyncBytes {
data.append(byte)
print(data.count)
}
I would have expected that, as soon as the task is cancelled, the download would have been stopped and, therefore, the for-await-in would stop processing bytes.
What am I missing here? Can these tasks not be effectively cancelled?
Canceling a URLSessionDataTask works fine with AsyncBytes. That having been said, even if the URLSessionDataTask is canceled, the AsyncBytes will continue to iterate through the bytes received prior to cancelation. But the data task does stop.
Consider experiment1:
#MainActor
class ViewModel: ObservableObject {
private let url: URL = …
private let session: URLSession = …
private var cancelButtonTapped = false
private var dataTask: URLSessionDataTask?
#Published var bytesBeforeCancel = 0
#Published var bytesAfterCancel = 0
func experiment1() async throws {
let (asyncBytes, _) = try await session.bytes(from: url)
dataTask = asyncBytes.task
var data = Data()
for try await byte in asyncBytes {
if cancelButtonTapped {
bytesAfterCancel += 1
} else {
bytesBeforeCancel += 1
}
data.append(byte)
}
}
func cancel() {
dataTask?.cancel()
cancelButtonTapped = true
}
}
So, I canceled after 1 second (at which point I had iterated through 2,022 bytes), and it continues to iterate through the remaining 14,204 bytes that had been received prior to the cancelation of the URLSessionDataTask. But the download does stop successfully. (In my example, the actual asset being downloaded was 74mb.) When using URLSession, the data comes in packets, so it takes AsyncBytes a little time to get through everything that was actually received before the URLSession request was canceled.
You might consider canceling the Swift concurrency Task, rather than the URLSessionDataTask. (I really wish they did not use the same word, “task”, to refer to entirely different concepts!)
Consider experiment2:
#MainActor
class ViewModel: ObservableObject {
private let url: URL = …
private let session: URLSession = …
private var cancelButtonTapped = false
private var task: Task<Void, Error>?
#Published var bytesBeforeCancel = 0
#Published var bytesAfterCancel = 0
func experiment2() async throws {
task = Task { try await download() }
try await task?.value
}
func cancel() {
task?.cancel()
cancelButtonTapped = true
}
func download() async throws {
let (asyncBytes, _) = try await session.bytes(from: url)
var data = Data()
for try await byte in asyncBytes {
try Task.checkCancellation()
if cancelButtonTapped { // this whole `if` statement is no longer needed, but I've kept it here for comparison to the previous example
bytesAfterCancel += 1
} else {
bytesBeforeCancel += 1
}
data.append(byte)
}
}
}
Without the try Task.checkCancellation() line, the behavior is almost the same as in experiment1. The cancelation of the Task with the AsyncBytes will result in the cancelation of the underlying URLSessionDataTask (but the sequence will continue to iterate through the bytes in the packets that were successfully received prior to cancelation). But with try Task.checkCancellation(), it will exit as soon as the Task is canceled.
TL;DR Read Rob's answer, but the iterator code and and the partial download code are still handy so I'm leaving this answer with corrections.
Okay so I spent some time on this because I'm about to try to write my own cancellable url stream object. and it appears that asyncBytes.task.cancel() is more along the lines of URLSession's finishTasksAndInvalidate() than invalidateAndCancel(). Since you are pointing your streaming task at a file that isn't really that large the URLSessionDataTask had already gotten the bytes in the buffer.
You can see this when you change up the function a bit (see Rob's example as well):
func test_funcCondition(timeOut:TimeInterval, url:URL, session:URLSession) async throws {
let (asyncBytes, _) = try await session.bytes(from: url)
let deadLine = Date.now + timeOut
var data = Data()
func someConditionCheck(_ deadline:Date) -> Bool {
Date.now > deadLine
}
for try await byte in asyncBytes {
if someConditionCheck(deadLine) {
asyncBytes.task.cancel()
print("trying to cancel...")
}
//Wrong type of task! Should not work. if Task.isCancelled { print ("cancelled") }
data.append(byte)
//just to reduce the amount of printing
if data.count % 100 == 0 {
print(data.count)
}
}
}
If you point the URL at "https://example.com/large_file.zip" like your example and make the time interval very short the function will print "trying to cancel..." between the time your marker hits and the file completes. It does NOT however, ever print "cancelled". (The task being cancelled is a URLSessionDataTask, not a Swift concurrency Task, that line never would have worked.)
If you point either what you wrote or this function at a Server-Sent-Event stream it will cancel out just fine. (While true, its not in contrast to the other behavior, which also works just fine. There are just bigger pauses in SSE data.)
If that isn't what you want, if you want to be able to start-stop streams mid-chunk, maybe explore a custom delegate (something I haven't done yet myself), or go work with AVFoundation if that's an option because they've thought a lot about working with large streaming files. I did not check making my own session and running session.invalidateAndCancel() on it instead, because that seems kind of extreme, but may be the way to go if you want to flush the buffer immediately.
The below will work to stop caring about the buffer immediately. It involves making a custom iterator. but it seems kind of quirky and may not in fact arrest the downloading (still cost users data rates and power). I haven't looked into how the stream protocol relates to the network protocol on that lower level, if you stop asking does it stop getting? I don't know. The cancel will arrest the stream allowing through the bytes that are already in the buffer, but your code won't get them. On my todo-list now is to look into how to change buffering policies.
Rob's code seems a nice way to go and advantage of a concurrency Task.
func test_customIterator(timeOut:TimeInterval, url:URL, session:URLSession) async throws {
let (asyncBytes, _) = try await session.bytes(from: url)
let deadLine = Date.now + timeOut
var data = Data()
func someConditionCheck(_ deadline:Date) -> Bool {
Date.now > deadLine
}
//could also be asyncBytes.lines.makeAsyncIterator(), etc.
var iterator = asyncBytes.makeAsyncIterator()
while !someConditionCheck(deadLine) {
//await Task.yield()
let byte = try await iterator.next()
data.append(byte!)
print(data.count)
}
//make sure to still tell URLSession you aren't listening anymore.
//It may auto-close but that's not how I roll.
asyncBytes.task.cancel()
}
let tap_out:TimeInterval = 0.0005
try await test_customIterator(timeOut: tap_out, url: URL(string:"https://example.com/large_file.zip")!, session: URLSession.shared)
Interesting flavor of behavior. Thanks for pointing it out. Also I didn't know that the task was already available (asyncBytes.task). Thanks for that. Incorrect. The asyncBytes.task is a URLSessionDataTask not a concurrency Task
UPDATED TO ADD:
To get part of the file explicitly
//https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
func requestInChunks(data:inout Data, url:URL, session:URLSession, offset:Int, length:Int) async throws {
var urlRequest = URLRequest(url: url)
urlRequest.addValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
let (asyncBytes, response) = try await
session.bytes(for: urlRequest, delegate: nil)
guard (response as? HTTPURLResponse)?.statusCode == 206 else { //NOT 200!!
throw APIngError("The server responded with an error.")
}
for try await byte in asyncBytes {
data.append(byte)
if data.count % 100 == 0 {
print(data.count)
}
}
}
Still think if my task on hand was about file downloading session.download would be my go to, but then there is file clean up, etc. so I get why not go there.

Actors with shared state

The following two actors share the same URLSession instance. The actors work in isolation, so is this a problem because each actor may change properties on the session at the same time? How can I protect against this?
actor ImageDownloaderA {
let session = URLSession.shared
func fetchImage() async throws -> Data {
let request = ...
return try await session.data(for: ...)
}
}
actor ImageDownloaderB {
let session = URLSession.shared
func fetchImage() async throws -> Data {
let request = ...
return try await session.data(for: ...)
}
}
struct Fetcher {
func fetcher() async throws {
let downloaderA = ImageDownloaderA()
let downloaderB = ImageDownloaderB()
_ = try await downloaderA.fetchImage()
_ = try await downloaderB.fetchImage()
}
}
URLSession is Sendable and is safe to be passed across concurrency domains. The documentation also explicitly tells us that it is thread-safe:
Thread Safety
The URL session API is thread-safe. You can freely create sessions and
tasks in any thread context. When your delegate methods call the
provided completion handlers, the work is automatically scheduled on
the correct delegate queue.
It should be noted that these two requests are not actually executing concurrently. This is not because of URLSession, but rather because this code will await the completion of ImageDownloaderA.fetchImage before even starting ImageDownloaderB.fetchImage. If we want concurrent execution, we might use async let or a task group.
Now, you’re not showing us what you’re doing with these two responses, but let’s imagine that for demonstration purposes you wanted them to simply print the number of bytes that each returned.
func fetcher() async throws {
let downloaderA = ImageDownloaderA()
let downloaderB = ImageDownloaderB()
async let data1 = downloaderA.fetchImage()
async let data2 = downloaderB.fetchImage()
let count1 = try await data1.count
let count2 = try await data2.count
print(count1, count2)
}
That will run the requests concurrently because we used async let and then only introduced await suspension points after both had been started. For details on async let, see SE-0317.
But, regardless, you can use the same URLSession in separate concurrency contexts without incident.

Control when a different number of requests are completed in Swift

I have the following code that runs about :
for resFoto in resFotosResenhaEscolhidas {
jsonRequestUploadImagem = ResFotosModel.createJsonFotoResenha(resFoto)
let requestUploadImagem: NSMutableURLRequest = serviceRepository.clientURLRequest(wsUploadImagem, typeAutho: .basic, parms: "", body: jsonRequestUploadImagem as Dictionary<String, AnyObject>)
serviceRepository.post(requestUploadImagem, retryLogin: true, completion: {isOk,msgError,httpCode,needLogin,response in
self.checkResponseUploadImagemFotoResenha(response as AnyObject, httpCode)
})
}
func checkResponseUploadImagemFotoResenha(_ response:AnyObject, _ httpCode:Int) {
if httpCode != 200 {
let string = String(data: response as! Data, encoding: .utf8)
print( string!+" \n Erro HTTP: "+String(httpCode) )
} else {
// httpCode == 200
let data: Data = response as! Data // received from a network request, for example
let jsonResponse = try? JSONSerialization.jsonObject(with: data, options: [])
print("json response upload foto")
print(jsonResponse!)
}
}
The serviceRepository.post just run a "urlSession.dataTask", but I wanna know how can I control when the completion of the request.
The "resFotosResenhaEscolhidas" object contains 0 to 4 array inside it depending on the call. So, the code runs and create from 0 to 4 requests.
If the 4 requests are running, I just wanna know how can I check when they are finished?
Look at using a DispatchGroup. You'd create a DispatchGroup when you get ready to begin making network calls.
You'd call enter() on your dispatch group each time you begin a new URLSession task (or other async task.) You'd call leave() on the dispatch group in the completion handler for each task.
After you've submitted your async tasks, you'd call the dispatch group's notify() method to submit a block that will execute once all your async tasks are complete. (It's important that you wait until you've submitted your async tasks before calling notify(). If you try to call it before submitting your tasks, it invokes it's closure immediately since no tasks are running.)
I wrote a little demo project that uses a DispatchGroup to monitor a set of async tasks. (In the demo the tasks just delay for a random time before completing, and generate a random number.)
It waits until they've all completed, and then indicates the task that returned the largest value.
You can check it out on Github: DispatchGroupDemo on github

Swift URLSession not working for localhost calls

I'm writing a basic API call in Swift using URLRequests, and for whatever reason my call is never executed. I have multiple calls to an external server API using the same method and the functionality is just as expected, however, for my server running locally I get no response or even behavior within the dataTask closure.
I have tried any relevant solutions I could find online such as: Swift URL Session and URL Request not working and Swift 3, URLSession dataTask completionHandler not called. But none of these solutions seem to fix my issue. I know that the local API is working as any calls through Postman go through without fail, yet even after using the Swift snippet provided by Postman, I get no functionality.
func doFoo(id: String, completion: #escaping ([[Float]]) -> ()) {
let semaphore = DispatchSemaphore(value: 0)
var request = URLRequest(url: URL(string: "127.0.0.1:8080/doFoo/\(id)")!, timeoutInterval: Double.infinity)
request.httpMethod = "GET"
print("THIS IS REACHED")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
print("THIS IS NEVER REACHED")
guard let data = data else {
self.semaphore.signal()
return
}
do {
// Decode json using JSONDecoder
// Call completion with JSON data
} catch {
print(error)
}
self.semaphore.signal()
}
task.resume()
self.semaphore.wait()
}
Other posts suggest that this could be an issue with the thread or execution completing before the closure is executed; while I am not super familiar with how the request executes and the behavior of semaphores, my understanding is that they are a way to request threads and prevent the above from happening.
If anyone more familiar with these topics could help me identify and understand why this issue is occurring, it would be greatly appreciated!

REST API calls not working in swift

I'm following this tutorial for making a simple REST API call in swift: https://grokswift.com/simple-rest-with-swift/
The problem I'm running into is that the data task completion handler next gets executed. When I'm debugging it step by step, it just jumps over the completion handler block. Nothing is printed in the console, either.
I've searched for other methods of making REST API calls, but they are all very similar to this one and not working, either.
Here is my code:
let endpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: endpoint) else {
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) -> Void in
guard error == nil else {
print("Error calling GET")
return
}
guard let responseData = data else {
print("Error receiving data")
return
}
do {
print ("Parsing response...")
}
}
task.resume()
Your code looks right to me. I tested it in a Playground and I'm getting the Parsing response... message printed to the console which makes me think the issue is elsewhere in your code or environment. I'd be happy to take a look at the whole project if you can post a Github link or something similar.
Here are the steps I would take to debug an issue like this:
1) Confirm my execution environment has an active internet connection. The Safari app can be used to confirm on iOS devices or the Simulator. Playgrounds can be tested by pasting the following lines.
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
print (try? String(contentsOf: url))
Look for a line in the console output similar to:
Optional("{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}")
2) Confirm the url is valid and returns data by pasting it into a web browser url bar and hitting enter. You will either see JSON printed in the browser or not.
3) Confirm my code is actually getting called when the application runs. You can do this with either breakpoints or print() statements. As OOPer2 pointed out asynchronous callback closures like that used in session.dataTask() execute in a different time than the rest of your code which is why "it just jumps over the completion handler block" while stepping through with the debugger. You'll need to put another breakpoint or print() statement inside the completion handler closure. I'd put the breakpoint on the guard error == nil else { line.
4) Make sure the application is still executing when the network request finishes and the completion handler closure executes. If your code is in a ViewController running in an iOS application it's probably fine, but if it's running in a Playground it may not be. Playgrounds by default stop execution once the last line of code has been evaluated which means the completion closure will never execute. You can tell a Playground to continue executing indefinitely by importing the PlaygroundSupport framework and setting needsIndefiniteExecution = true on the current Playground page. Paste the entire code block below into a Playground to see it in action:
import Foundation
import PlaygroundSupport
// Keep executing the program after the last line has evaluated so the
// closure can execute when the asynchronous network request finishes.
PlaygroundPage.current.needsIndefiniteExecution = true
// Generic Result enum useful for returning values OR an error from
// asynchronous functions.
enum Result<T> {
case failure(Error)
case success(T)
}
// Custom Errors to be returned when something goes wrong.
enum NetworkError: Error {
case couldNotCreateURL(for: String)
case didNotReceiveData
}
// Perform network request asynchronous returning the result via a
// completion closure called on the main thread.
//
// In really life the result type will not be a String, it will
// probably be an array of custom structs or similar.
func performNetworkRequest(completion: #escaping (Result<String>)->Void ) {
let endpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: endpoint) else {
let error = NetworkError.couldNotCreateURL(for: endpoint)
completion(Result.failure(error))
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) -> Void in
// This closure is still executing on a background thread so
// don't touch anything related to the UI.
//
// Remember to dispatch back to the main thread when calling
// the completion closure.
guard error == nil else {
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.failure(error!))
}
return
}
guard let responseData = data else {
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.failure(NetworkError.didNotReceiveData))
}
return
}
// Parse response here...
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.success("Sucessfully parsed results"))
}
}
task.resume()
}
performNetworkRequest(completion: { result in
// The generic Result type makes handling the success and error
// cases really nice by just using a switch statement.
switch result {
case .failure(let error):
print(error)
case .success(let parsedResponse):
print(parsedResponse)
}
})
Why you dont use this Library Alamofire is an HTTP networking library written in Swift.
Add this line to your Podfile
pod 'Alamofire', '~> 4.4'
Then, run the following command:
pod install
Then in your ViewController file:
import Alamofire
Alamofire.request("https://jsonplaceholder.typicode.com/todos/1").responseJSON { response in
print("Request: \(String(describing: response.request))") // original url request
print("Response: \(String(describing: response.response))") // http url response
print("Result: \(response.result)") // response serialization result
if let json = response.result.value {
print("JSON: \(json)") // serialized json response
}
If let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)") // original server data as UTF8 string
}
}
And in here are an example of how to parse the result.
https://github.com/CristianCardosoA/JSONParser
For more info about Alamofire:
https://github.com/Alamofire/Alamofire
I hope this help.