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.
Related
Picture an image loading function with a closure completion. Let's say it returns a token ID that you can use to cancel the asynchronous operation if needed.
#discardableResult
func loadImage(url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) -> UUID? {
if let image = loadedImages[url] {
completion(.success(image))
return nil
}
let id = UUID()
let task = URLSession.shared.dataTask(with: url) { data, response, error in
defer {
self.requests.removeValue(forKey: id)
}
if let data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.loadedImages[url] = image
completion(.success(image))
}
return
}
if let error = error as? NSError, error.code == NSURLErrorCancelled {
return
}
//TODO: Handle response errors
print(response as Any)
completion(.failure(.loadingError))
}
task.resume()
requests[id] = task
return id
}
func cancelRequest(id: UUID) {
requests[id]?.cancel()
requests.removeValue(forKey: id)
print("ImageLoader: cancelling request")
}
How would we accomplish this (elegantly) with swift concurrency? Is it even possible or practical?
I haven't done much testing on this, but I believe this is what you're looking for. It allows you to simply await an image load, but you can cancel using the URL from somewhere else. It also merges near-simultaneous requests for the same URL so you don't re-download something you're in the middle of.
actor Loader {
private var tasks: [URL: Task<UIImage, Error>] = [:]
func loadImage(url: URL) async throws -> UIImage {
if let imageTask = tasks[url] {
return try await imageTask.value
}
let task = Task {
// Rather than removing here, you could skip that and this would become a
// cache of results. Making that correct would take more work than the
// question asks for, so I won't go into it
defer { tasks.removeValue(forKey: url) }
let data = try await URLSession.shared.data(from: url).0
guard let image = UIImage(data: data) else {
throw DecodingError.dataCorrupted(.init(codingPath: [],
debugDescription: "Invalid image"))
}
return image
}
tasks[url] = task
return try await task.value
}
func cancelRequest(url: URL) {
// Remove, and cancel if it's removed
tasks.removeValue(forKey: url)?.cancel()
print("ImageLoader: cancelling request")
}
}
Calling it looks like:
let image = try await loader.loadImage(url: url)
And you can cancel a request if it's still pending using:
loader.cancelRequest(url: url)
A key lesson here is that it is very natural to access task.value multiple times. If the task has already completed, then it will just return immediately.
Return a task in a tuple or other structure.
In the cases where you don't care about the ID, do this:
try await imageTask(url: url).task.value
private var requests: [UUID: Task<UIImage, Swift.Error>] = [:]
func imageTask(url: URL) -> (id: UUID?, task: Task<UIImage, Swift.Error>) {
switch loadedImages[url] {
case let image?: return (id: nil, task: .init { image } )
case nil:
let id = UUID()
let task = Task {
defer { requests[id] = nil }
guard let image = UIImage(data: try await URLSession.shared.data(from: url).0)
else { throw Error.loadingError }
try Task.checkCancellation()
Task { #MainActor in loadedImages[url] = image }
return image
}
requests[id] = task
return (id: id, task: task)
}
}
Is it even possible or practical?
Yes to both.
As I say in a comment, I think you may be missing the fact that a Task is an object you can retain and later cancel. Thus, if you create an architecture where you apply an ID to a task as you ask for the task to start, you can use that same ID to cancel that task before it has returned.
Here's a simple demonstration. I've deliberately written it as Playground code (though I actually developed it in an iOS project).
First, here is a general TimeConsumer class that wraps a single time-consuming Task. We can ask for the task to be created and started, but because we retain the task, we can also cancel it midstream. It happens that my task doesn't return a value, but that's neither here nor there; it could if we wanted.
class TimeConsumer {
var current: Task<(), Error>?
func consume(seconds: Int) async throws {
let task = Task {
try await Task.sleep(for: .seconds(seconds))
}
current = task
_ = await task.result
}
func cancel() {
current?.cancel()
}
}
Now then. In front of my TimeConsumer I'll put a TaskVendor actor. A TimeConsumer represents just one time-consuming task, but a TaskVendor has the ability to maintain multiple time-consuming tasks, identifying each task with an identifier.
actor TaskVendor {
private var tasks = [UUID: TimeConsumer?]()
func giveMeATokenPlease() -> UUID {
let uuid = UUID()
tasks[uuid] = nil
return uuid
}
func beginTheTask(uuid: UUID) async throws {
let consumer = TimeConsumer()
tasks[uuid] = consumer
try await consumer.consume(seconds: 10)
tasks[uuid] = nil
}
func cancel(uuid: UUID) {
tasks[uuid]??.cancel()
tasks[uuid] = nil
}
}
That's all there is to it! Observe how TaskVendor is configured. I can do three things: I can ask for a token (really my actual TaskVendor needn't bother doing this, but I wanted to centralize everything for generality); I can start the task with that token; and, optionally, I can cancel the task with that token.
So here's a simple test harness. Here we go!
let vendor = TaskVendor()
func test() async throws {
let uuid = await vendor.giveMeATokenPlease()
print("start")
Task {
try await Task.sleep(for: .seconds(2))
print("cancel?")
// await vendor.cancel(uuid: uuid)
}
try await vendor.beginTheTask(uuid: uuid)
print("finish")
}
Task {
try await test()
}
What you will see in the console is:
start
[two seconds later] cancel?
[eight seconds after that] finish
We didn't cancel anything; the word "cancel?" signals the place where our test might cancel, but we didn't, because I wanted to prove to you that this is working as we expect: it takes a total of 10 seconds between "start" and "finish", so sure enough, we are consuming the expected time fully.
Now uncomment the await vendor.cancel line. What you will see now is:
start
[two seconds later] cancel?
[immediately!] finish
We did it! We made a cancellable task vendor.
I'm including one possible answer to the question, for the benefit of others. I'll leave the question in place in case someone has another take on it.
The only way that I know of having a 'one-shot' async method that would return a token before returning the async result is by adding an inout argument:
func loadImage(url: URL, token: inout UUID?) async -> Result<UIImage, Error> {
token = UUID()
//...
}
Which we may call like this:
var requestToken: UUID? = nil
let result = await imageLoader.loadImage(url: url, token: &requestToken)
However, this approach and the two-shot solution by #matt both seem fussy, from the api design standpoint. Of course, as he suggests, this leads to a bigger question: How do we implement cancellation with swift concurrency (ideally without too much overhead)? And indeed, using tasks and wrapper objects seems unavoidable, but it certainly seems like a lot of legwork for a fairly simple pattern.
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed last year.
I'm new at Swift and that's why i need your help. So I have a function which should send request and return a value
func getAnswer() -> String? {
var answer: String?
guard let url = URL(string: "https://8ball.delegator.com/magic/JSON/_") else { return nil }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode == 200 else { return }
do {
let model = try JSONDecoder().decode(Answer.self, from: data)
DispatchQueue.main.async {
answer = model.magic.answer
}
} catch let error {
fatalError(error.localizedDescription)
}
}.resume()
return answer
}
but it always returns nil.
I suppose problem is here
DispatchQueue.main.async {
answer = model.magic.answer
}
How can I fix it?
In order to know what is happening here, you need to learn about #escaping functions in swift, here is some link1 together with taking function as another functions parameter link2 written in part "Function Types as Parameter Types" , closures in Swift link3 and
Here is what is happening simplified and explained step by step :
you call getAnswer()
variable answer gets initialized with value nil by declaring answer: String?
URLSession.shared.dataTask is called and it is taking as an argument another function - closure (Data?, URLResponse?, Error?) -> Void . Also URLSession.shared.dataTask is executed on different thread and is not returning yet, but will return right after it receives response from server, which can take any time (but usually milliseconds) and will basically happen after your getAnswer() function is returning value.
your getAnswer() immediately returns value of answer which is currently nil
if you get any data from server, or server could not be reached, your URLSession.shared.dataTask function executes your code in closure. This is the code it will execute:
guard let data = data, error == nil else {
return
}
guard let response = response as? HTTPURLResponse else { return }
guard response.statusCode == 200 else { return }
do {
let model = try JSONDecoder().decode(Answer.self, from: data)
DispatchQueue.main.async {
answer = model.magic.answer
}
} catch let error {
fatalError(error.localizedDescription)
}
Your problem lies in how swift executes closures. When you call
URLSession.shared.dataTask(with: url) {
// Closure code here
}
return answer
Your "Closure code here" doesn't get called until the endpoint "https://8ball.delegator.com/magic/JSON/_" actually gives a response. However, you've promised swift that your function will return an optional string immediately after the serial code of your function has completed. For this reason, by the time your "Closure code here" has run, and your "answer" variable has been updated with the correct value, your function is long gone, and has already returned a value (which in this case is whatever you've set it to at the beginning - nil).
You can fix this issue in one of two ways.
Swift's new concurrency system
By defining your own closure.
Swift's new concurrency system
You can define your function as async, meaning that the function won't have to return a value in serial, as follows.
enum GetAnswerError: Error {
case invalidURL
}
func getAnswer() async throws -> String {
var answer: String?
guard let url = URL(string: "https://8ball.delegator.com/magic/JSON/_") else {
throw GetAnswerError.invalidURL
}
// Your function will suspend here and probably be moved to a different thread. It will resume once a response has been received from the endpoint.
let (data, _) = try await URLSession.shared.dataTask(with: url)
let parsedData = try JSONDecoder().decode(Answer.self, from: data)
return parsedData.magic.answer
}
When you call this function, you'll have to do so from an environment which swift can suspend. This means you'll call the function from either another async function like so
func anotherFunction() async throws -> Bool {
let answer = try await getAnswer()
// Run some code here
return answer == "YES" // Return some useful value
}
or from a Task object like so
Task {
// Note that because the function getAnswer() can throw errors, you'll have to handle them when you call the function. In this case, I'm handling them by using try?, which will simply set answer to nil if an error is thrown.
let answer = try? await getAnswer()
}
Note that when you call code in a task, you must be using the return value's from within the scope of the task. If you try to do something like this
func getAnswerTheSecond() -> String? {
var answer: String? = nil
Task {
let receivedAnswer = try? await getAnswer()
answer = receivedAnswer
}
return answer
}
You'll just end up back where you started, where swift immediately returns the nil value because your code is ran in serial. To fix this, run the relevant code on the "answer" from wherever it is needed within the task. If you are using the "answer" to update a SwiftUI view that might look like this.
struct ContentView: View {
#State var answer: String = ""
// This is the function that I've written earlier
func getAnswer() async throws -> String {
// Make URL Request
// Return the value
}
var body: some View {
Text(self.answer)
.onAppear{
Task{
let result = try? await self.getAnswer()
self.answer = result
}
}
}
}
Defining your own closure
You can define your own closure to handle the URL response; however, because of swift's new concurrency framework, this is probably not the right way to go.
If you'd like to go this way, do a google search for "Swift closures", and you'll find what you need.
I'm trying to write a function using Swift and Vapor but I don't understand why one statement gets printed before the other:
// Logout user
func logout(_ req: Request) throws -> Future<APIResponseMessage> {
let userID = self.checkAccessToken(req: req)
// Delete access token here
let apiResponseMessage = APIResponseMessage()
apiResponseMessage.message = "success"
apiResponseMessage.userID = userID
return apiResponseMessage.create(on: req)
}
func checkAccessToken(req: Request) -> Int {
let bearerAuthorization = req.http.headers.bearerAuthorization
guard let _bearerAuthorization = bearerAuthorization else {
// Works fine
print("no bearer incluced")
return 0
}
let _ = AccessToken.query(on: req).filter(\.accessToken == _bearerAuthorization.token).first().map(to: Int.self) { queriedAccessToken in
// This should be first
print("This gets printed second")
return queriedAccessToken!.userID!
}
// This should be second
print("This gets printed first")
return 0
}
Can anyone tell me how to make the second print statement wait until the first one is completed?
Right now it's causing my logout function to run with userID == 0 when this shouldn't be the case
As #nathan said, this is due to your code being async. Your .map callback is like the closure you pass into to a URLSession.dataTask when making request's to an external API for an iOS app.
Vapor uses a slightly different async model then what you use in iOS though, using promises and futures instead of callback closures. You can read about them in the docs.
In your case, you want to return the userID you get from the AccessToken query. To do this, you first need to change your method's return type from Int to Future<Int>. Then, instead of assigning the result of the .map call to _, you can return it from the method:
func checkAccessToken(req: Request) -> Future<Int> {
let bearerAuthorization = req.http.headers.bearerAuthorization
guard let _bearerAuthorization = bearerAuthorization else {
return req.future(0)
}
return AccessToken.query(on: req).filter(\.accessToken == _bearerAuthorization.token).first().map(to: Int.self) { queriedAccessToken in
return queriedAccessToken!.userID!
}
}
I would suggest you look into error handling for your queriedAccessToken and userID values so you aren't force-unwrapping them.
How i can return value from function used Alamofire . i try to print outside .responseJSON the value in ArrData is not set but i try print inside it work
this code:
func getDept()->NSMutableArray
{
var ArrData:NSMutableArray = []
let url = "http://www.xxxxxxxxxxxxx.com"
Alamofire.request(.GET, url).responseJSON { response in
let json = JSON(response.result.value!)
let count = json.count
for var index = 0; index < count;index++
{
ArrData.addObject(json[index]["dept"].stringValue)
}
}
return ArrData
}
it i good idea to check at least README.md of the framework which you are going to use in your code
Networking in Alamofire is done asynchronously. Asynchronous
programming may be a source of frustration to programmers unfamiliar
with the concept, but there are very good reasons for doing it this
way.
Rather than blocking execution to wait for a response from the server,
a callback is specified to handle the response once it's received. The
result of a request is only available inside the scope of a response
handler. Any execution contingent on the response or data received
from the server must be done within a handler.
Try to use a handler like this and a callback:
func getopt(callback:(array: [String]) -> void ){
func completion(request: NSURLRequest?, response:NSHTTPURLResponse?,result:Result<AnyObject>){
if let rdata = result.value{
let data = JSON(rdata)
print(data)
let myArray = [String]
let objects = data.array
for object in objects{
myArray.append(object)
}
callback(myArray)
}
}
let url = "http://www.xxxxxxxxxxxxx.com"
Alamofire.request(.GET,url),
encoding: .JSON).responseJSON(completionHandler: completion)
}
You pass the array to your callback So when you call getopt where you call it you can print the array. Some like this:
func something (){
getopt(callback)
}
func callback(array:[String]){
print array[0]
}
My idea is easy:
1. I have a ViewController with button
2. After click button i want get data from my site
3. I want put indicator info about data are downloading
4. After finished show message OK or NOT OK
I have a class CCCLass
class CCClass {
let CCClass BaseURL: NSURL?
var seed: String?
init() {
CCClassBaseURL = NSURL(string: "mysiteblabla")
}
func getData() {
if let CCClassURL = NSURL(string: "", relativeToURL: CCClassBaseURL ) {
let networkOperation = NetworkOperation(url: CCClassURL)
networkOperation.downloadJSONFromURL {
(let JSONDictionary) in
print(JSONDictionary?["seed"])
self.seed = JSONDictionary?["seed"] as? String
}
} else {
print("Cold not construct a valid URL")
}
}
}
On click button i have
let ccClass = CCClass()
ccClass.getData() {
(let test) in
dispatch_async(dispatch_get_main_queue()) {
// go to something on the main thread
print(ccClass.seed)
}
}
And i have error : Cannot invoke 'getData' with an argument list of type '((_) -> _)'
And i am not sure should I in function getData create Array and return to main thread ? after data will be download i want use this to next http query.
You're calling getData with a trailing closure, but the definition of getData doesn't take a closure parameter (or call it). What you have will work if you change the method to take the closure and use it (probably to pass the download result back).
You have to declare a closure parameter in your method if you want to call it with a trailing closure:
func getData( completion:()->() ) {
if let CCClassURL = NSURL(string: "", relativeToURL: CCClassBaseURL ) {
let networkOperation = NetworkOperation(url: CCClassURL)
networkOperation.downloadJSONFromURL {
(let JSONDictionary) in
print(JSONDictionary?["seed"])
self.seed = JSONDictionary?["seed"] as? String
completion()
}
} else {
print("Cold not construct a valid URL")
completion()
}
}
A good rule of thumb to follow is that any method that takes some kind of "completion" closure should always call that closure even if the operation fails. So make sure you call completion() in your else block where URL construction failed, or if a network error occurred, or if a JSON parsing error occurred.