Using Swift 4, I have this code that attempts a POST request to a REST API:
spinner.startAnimation(self)
btnOk.isEnabled = false
btnCancel.isEnabled = false
attemptPost()
spinner.stopAnimation(self)
btnOk.isEnabled = true
btnCancel.isEnabled = true
The function that does this (Constants and Request are classes that I created that create the request objects and hold frequently used data):
func attemptPost() {
let url = Constants.SERVICE_URL + "account/post"
let body: [String : Any] =
["firstName": txtFirstName.stringValue,
"lastName": txtLastName.stringValue,
"email": txtEmail.stringValue,
"password": txtPassword.stringValue];
let req = Request.create(urlExtension: url, httpVerb: Constants.HTTP_POST, jsonBody: body)
let task = URLSession.shared.dataTask(with: req) { data, response, err in
guard let data = data, err == nil else {
// error
return
}
if let resp = try? JSONSerialization.jsonObject(with: data) {
// success
}
}
task.resume()
}
Since the task that does this runs asynchronously, there is no sequential way that I can update the UI once the call to attemptPost() returns. And since the UI components are on the main thread, I can't directly update the components from the task that makes the request.
In C# it works the same way; there is a BackgroundWorker class in which you can safely update the UI components to avoid a "Cross-thread operation not valid" error.
I'm trying to find an example that accomplishes more or less the same thing, in which a "wait" state is established, the task runs, and upon task completion, the main thread is notified that the task is done so that the wait state can be changed.
But I'm still having trouble understanding how this all comes together in Swift. I've looked around and seen information about the handlers that are invoked from within URLSessionDataTask and stuff about GCD, but I'm still not able to connect the dots.
And is GCD even relevant here since the URLSessionDataTask task is asynchronous to begin with?
Any help is appreciated.
If I understood correctly you might try this solution:
spinner.startAnimation(self)
btnOk.isEnabled = false
btnCancel.isEnabled = false
attemptPost { (success) in
DispatchQueue.main.async {
spinner.stopAnimation(self)
btnOk.isEnabled = true
btnCancel.isEnabled = true
}
// UI wise, eventually you can do something with 'success'
}
func attemptPost(_ completion:#escaping (Bool)->())
let url = Constants.SERVICE_URL + "account/post"
let body: [String : Any] =
["firstName": txtFirstName.stringValue,
"lastName": txtLastName.stringValue,
"email": txtEmail.stringValue,
"password": txtPassword.stringValue];
let req = Request.create(urlExtension: url, httpVerb: Constants.HTTP_POST, jsonBody: body)
let task = URLSession.shared.dataTask(with: req) { data, response, err in
guard let data = data, err == nil else {
completion(false)
return
}
if let resp = try? JSONSerialization.jsonObject(with: data) {
completion(true)
}
}
task.resume()
}
so the idea is executing from attemptPost a block which will run asynchronously into the main thread your UI stuff
Related
I'm trying to make a fairly simple API call in Swift but, for some reason, my dataTask code is not running. I've made sure that the .resume() is there. This code has worked in the past but, something has changed recently and I don't know what it is. The only thing I can think of is the url. I've changed the ingredients but, when putting the url into a browser, it returns JSON data normally. When running this function, I get two "Outside URLSession.shared.dataTask....." messages in a row with nothing in between, indicating that the URLSession block of code isn't running. I'm a little new to APIs so, any help would be greatly appreciated. Please let me know if there's any more information I can provide. Also, I'm on an older MacBook and am using Swift5 if that makes a difference. Thanks!
let url: URL! = URL(string: "https://api.spoonacular.com/recipes/findByIngredients?ingredients=" + ingredientString + "&apiKey=aaabbbccc111222333")
print("URL: " + url.absoluteString)
let request = URLRequest(url: url)
// Make the API call
print("Outide URLSession.shared.dataTask.....")
let session = URLSession.shared.dataTask(with: request) { data, response, error in
print("Inside URLSession.shared.dataTask.....")
DispatchQueue.main.async {
print("Inside DispatchQueue.main.async....")
if data == nil {
print("No data recieved.")
}
print("data != nil.... Moving on to JSONDecoder....")
self.model = try! JSONDecoder().decode([RecipeSearchElement].self, from: data!)
}
}
session.resume()
print("Outside URLSession.shared.dataTask.....")
Unrelated to your immediate question at hand (which I answered elsewhere), I would advise a few changes to the routine:
One should not build a URL through string interpolation. Use URLComponents. If, for example, the query parameter included a space or other character not permitted in a URL, URLComponents will percent-encode it for you. If do not percent-encode it properly, the building of the URL will fail.
I would avoid try!, which will crash the app if the server response was not what you expected. One should use try within a do-catch block, so it handles errors gracefully and will tell you what is wrong if it failed.
I would recommend renaming the URLSessionDataTask to be task, or something like that, to avoid conflating “sessions” with the “tasks” running on that session.
I would not advise updating the model from the background queue of the URLSession. Fetch and parse the response in the background queue and update the model on the main queue.
Thus:
var components = URLComponents(string: "https://api.spoonacular.com/recipes/findByIngredients")
components?.queryItems = [
URLQueryItem(name: "ingredients", value: ingredientString),
URLQueryItem(name: "apiKey", value: "aaabbbccc111222333")
]
guard let url = components?.url else {
print("Unable to build URL")
return
}
// Make the API call
let task = URLSession.shared.dataTask(with: url) { data, _, error in
DispatchQueue.main.async {
guard error == nil, let data = data else {
print("No data received:", error ?? URLError(.badServerResponse))
return
}
do {
let model = try JSONDecoder().decode([RecipeSearchElement].self, from: data)
DispatchQueue.main.async {
self.model = model
}
} catch let parseError {
print("Parsing error:", parseError, String(describing: String(data: data, encoding: .utf8)))
}
}
}
task.resume()
In a more advanced observation, I would never have a network call update the model directly. I would leave that to the caller. For example, you could use a completion handler pattern:
#discardableResult
func fetchIngredients(
_ ingredientString: String,
completion: #escaping (Result<[RecipeSearchElement], Error>) -> Void
) -> URLSessionTask? {
var components = URLComponents(string: "https://api.spoonacular.com/recipes/findByIngredients")
components?.queryItems = [
URLQueryItem(name: "ingredients", value: ingredientString),
URLQueryItem(name: "apiKey", value: "aaabbbccc111222333")
]
guard let url = components?.url else {
completion(.failure(URLError(.badURL)))
return nil
}
// Make the API call
let task = URLSession.shared.dataTask(with: url) { data, _, error in
print("Inside URLSession.shared.dataTask.....")
DispatchQueue.main.async {
guard error == nil, let data = data else {
DispatchQueue.main.async {
completion(.failure(error ?? URLError(.badServerResponse)))
}
return
}
do {
let model = try JSONDecoder().decode([RecipeSearchElement].self, from: data)
DispatchQueue.main.async {
completion(.success(model))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
}
task.resume()
return task
}
And then the caller could do:
fetchIngredients(ingredientString) { [weak self] result in
switch result {
case .failure(let error): print(error)
case .success(let elements): self?.model = elements
}
}
This has two benefits:
The caller now knows when the model is updated, so you can update your UI at the appropriate point in time (if you want).
It maintains a better separation of responsibilities, architecturally avoiding the tight coupling of the network layer with that of the view or view model (or presenter or controller) layers.
Note, I am also returning the URLSessionTask object in case the caller would like to cancel it at a later time, but I made it an #discardableResult so that you do not have to worry about that if you are not tackling cancelation at this point.
If you (a) are reaching the “outside” message, but not seeing the “inside” message; and (b) are absolutely positive that you are reaching the resume statement, it is one of a few possibilities:
The app may be terminating before the asynchronous request has time to finish. This can happen, for example, if this is a command-line app and you are allowing the app to quit before the asynchronous request has a chance to finish. If you want a command-line app to wait for a network request to finish, you might run a RunLoop that does not exit until the network request is done.
It can also happen if you use a playground and neglect to set needsIndefiniteExecution:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
For the sake of completeness, there are a few other, less common, possibilities:
You have some other network request whose completion handler is blocked/deadlocked, thereby preventing anything else from running on the URLSession dedicated, serial, queue.
You have thread explosion somewhere else in your code, exhausting the limited pool of worker threads, preventing other tasks/operations from being able to get an available worker thread.
I am utilizing URLSessions which are asynchronous in nature.
This works well when there is only one call for a session.
When I need to execute multiple (serial) calls, where the results need to be combined in the order of execution, it makes program logic painful and error prone.
I am also blocking the main thread, which isn't good.
Constraints
Moving to next task may not occur before 4 second elapses, though it can be more.
Utilizing Monterey (OS upgrade required), so using the let (data, response) = try await session.data(from: url) is not optional.
If tasks are executed faster than every 4 seconds, the result is a server side error, forcing a retry.
Task execution
Execute task -- if the task concludes in less than 4 seconds, wait
for the difference so the next task does not execute before the 4
seconds elapses.
Repeat process until all task have been completed.
Combine the results
My current process utilizes semaphores or dispatchGroups but both block the main thread.
Is there a way to get synchronous behavior without blocking the main thread?
func getDataFromInput_Sync(authToken: String, transformedText: String ) -> (Data?, URLResponse?, Error?)
{
/*
By default, transactions are asynchronous, so they return data while the rest of the program continues.
Changing the behavior to synchronous requires blocking to wait for the outcome. It affords us the
ability to manage program flow inline. This methods forces synchronous behavior and the output, which
are returned via a tuple, types: (Data?, URLResponse?, Error?)
*/
var outData : Data?
var outError : Error?
var urlResponse : URLResponse?
let targetURL = "https://..."
let urlconfig = URLSessionConfiguration.ephemeral
// set the timeout to a high number, if we are prepared to wait that long. Otherwise the session will timeout.
urlconfig.timeoutIntervalForRequest = 120
urlconfig.timeoutIntervalForResource = 120
let urlSession = URLSession(configuration: urlconfig)
// let dispatchGroup = DispatchGroup()
let semaphore = DispatchSemaphore(value: 0)
// ephermeral doesnt write cookies, cache or credentials to disk
guard let url = URL(string: targetURL),
let httpBodyData = transformedText.data(using: .utf8) else { return (nil,nil,nil) }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = httpBodyData
request.addValue("Token " + authToken, forHTTPHeaderField: "Authorization")
// Perform HTTP Request
let task = (urlSession).dataTask(with: request) { (data, response, error) in
guard error == nil
else {
print("we have an error: \(error!.localizedDescription)")
return
}
guard let data = data else { print("Empty data"); return }
outData = data
urlResponse = response
outError = error
// dispatchGroup.leave()
semaphore.signal()
}
task.resume()
semaphore.wait()
// dispatchGroup.enter()
// task.resume()
// dispatchGroup.wait()
return (outData, urlResponse, outError)
}
func testServerRequest()
{
let sentences = ["Example Sentence1","Example Sentence2","Example Sentence3"] //...array to process
for (_,thisString) in sentences.enumerated()
{
let timeTestPoint = Date()
let futureSecs = 4.0
let (data, urlResponse, error) = getDataFromInput_Sync(authToken: authToken, transformedText: thisString )
let elapsed = timeTestPoint.timeIntervalSinceNow * -1 // time elapsed between request and result
// act on the data received
// executing the next request before futureSecs will cause an error, so pause
let delayAmt = futureSecs - elapsed
Thread.sleep(forTimeInterval: delayAmt)
}
}
Make it in background queue, like
DispatchQueue.global(qos: .background).async {
testServerRequest()
}
For one week I have been trying to get a string returned from dataTask().
I already read a lot here on StackOverFlow and also from serval sites where they tackle this topic. For example, this one. So I already understand that it's that the dataTask doesn't directly return values, cause it happens on different threads and so on. I also read about closures and completion handlers. I really got the feeling that I actually already got a little clue what this is about. But I can't get it to work.
So this is my code. I just post the whole code so no-one needs to worry that the problem sticks in a part which I don't show. Everything is working fine until I try to return a value and save it for example in a variable:
func requestOGD(code gtin: String, completion: #escaping (_ result: String) -> String) {
// MARK: Properties
var answerList: [String.SubSequence] = []
var answerDic: [String:String] = [:]
var product_name = String()
var producer = String()
// Set up the URL request
let ogdAPI = String("http://opengtindb.org/?ean=\(gtin)&cmd=query&queryid=400000000")
guard let url = URL(string: ogdAPI) else {
print("Error: cannot create URL")
return
}
let 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
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result, which is String. It willbecome split and placed in a dictionary
do {
let answer = (String(decoding: responseData, as: UTF8.self))
answerList = answer.split(separator: "\n")
for entry in answerList {
let entry1 = entry.split(separator: "=")
if entry1.count > 1 {
let foo = String(entry1[0])
let bar = String(entry1[1])
answerDic[foo] = "\(bar)"
}
}
if answerDic["error"] == "0" {
product_name = answerDic["detailname"]!
producer = answerDic["vendor"]!
completion(product_name)
} else {
print("Error-Code der Seite lautet: \(String(describing: answerDic["error"]))")
return
}
}
}
task.resume()
Here I call my function, and no worries, I also tried to directly return it to the var foo, also doesn't work The value only exists within the closure:
// Configure the cell...
var foo:String = ""
requestOGD(code: listOfCodes[indexPath.row]) { (result: String) in
print(result)
foo = result
return result
}
print("Foo:", foo)
cell.textLabel?.text = self.listOfCodes[indexPath.row] + ""
return cell
}
So my problem is, I have the feeling, that I'm not able to get a value out of a http-request.
You used a completion handler in your call to requestOGD:
requestOGD(code: listOfCodes[indexPath.row]) {
(result: String) in
// result comes back here
}
But then you tried to capture and return that result:
foo = result
return result
So you're making the same mistake here that you tried to avoid making by having the completion handler in the first place. The call to that completion handler is itself asynchronous. So you face the same issue again. If you want to extract result at this point, you would need another completion handler.
To put it in simple terms, this is the order of operations:
requestOGD(code: listOfCodes[indexPath.row]) {
(result: String) in
foo = result // 2
}
print("Foo:", foo) // 1
You are printing foo before the asynchronous code runs and has a chance to set foo in the first place.
In the larger context: You cannot use any asynchronously gathered material in cellForRowAt. The cell is returned before the information is gathered. That's what asynchronous means. You can't work around that by piling on further levels of asynchronicity. You have to change your entire strategy.
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.
In my project for OS X Command Line Tool using Swift 3.0 (Beta 2) I need to convert HTML data to String from multiple URLs. There is a problem in use of such function with many background tasks (it's not working except the main thread, so maybe there is more elegant way to control completion of all tasks and read HTML data in such tool with or without parsers that I need for Swift 3 and Mac OS X (Linux in the near future)):
func html2text (html: String, usedEncoding: String.Encoding) -> String {
let data = html.data(using: usedEncoding)!
if let htmlString = AttributedString(html: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: usedEncoding.rawValue], documentAttributes: nil)?.string {
return htmlString
} else {
return ""
}
}
So I read data first into an Array, waiting when all DataTasks finished and then converting it in the main thread. Also using global variable (Set of urls) to control completion of each task:
import Foundation
import WebKit
var urlArr = [String]()
var urlSet = Set<String>()
var htmlTup : [(url : String, html : String, encoding : String.Encoding)] = []
let session = URLSession.shared
For-in loop with multiple URLSession DataTasks
for myurl in urlArr {
if urlSet.insert(myurl).inserted {
print ("Loading \(myurl)...")
let inputURL = URL(string: myurl)!
let task = session.dataTask(with: inputURL, completionHandler: {mydata, response, error in
Read Encoding from HTML First
var usedEncoding = String.Encoding.utf8
if let encodingName = response!.textEncodingName {
let encoding = CFStringConvertIANACharSetNameToEncoding(encodingName)
if encoding != kCFStringEncodingInvalidId {
usedEncoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(encoding))
}
}
Do some work with HTML String and read data into an Array
if let myString = String(data: mydata!, encoding: usedEncoding) {
htmlTup += [(url: myurl,html: myString, encoding: usedEncoding)]
}
// The end of task removing URL from Set
urlSet.remove(myurl)
})
//Run Task
task.resume()
}
}
}
Waiting for tasks to complete and convert HTML to text
while !urlSet.isEmpty {
// Do nothing
}
for (url,html,encoding) in htmlTup {
print ("Writing data from \(url)...")
print (html2text(html: html, usedEncoding: encoding))
}
Update 1: RunLoop in the main thread from this
Such code to check when each task finished:
var taskArr = [Bool]()
let task = session.dataTask(with: request) { (data, response, error) in
}
taskArr.removeLast()
}
taskArr.append(true)
task.resume()
// Waiting for tasks to complete
let theRL = RunLoop.current
while !taskArr.isEmpty && theRL.run(mode: .defaultRunLoopMode, before: .distantFuture) { }
You can't just spin in a busy loop waiting for results, because you're blocking the main run loop/thread/dispatch queue by doing that.
Instead, return at that point, thus allowing the main run loop to run. Then, in your completion handler, check to see if you've gotten all the responses you're expecting, and if so, do the stuff that you currently have after that busy wait while loop.