-1

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?

qeqolla
  • 11
  • 2

2 Answers2

0

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.

  1. Swift's new concurrency system
  2. 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.

SMarx
  • 19
  • 2
0

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 :

  1. you call getAnswer()

  2. variable answer gets initialized with value nil by declaring answer: String?

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

  4. your getAnswer() immediately returns value of answer which is currently nil

  5. 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)
         }
    
Mr.SwiftOak
  • 1,469
  • 3
  • 8
  • 19