0

I'm new in swift and I'm struggling trying to understand how to use DispatchQueue to make a network request. Here's what a have so far:

Network Request with a function request:

class NetworkRequest {

var host: String = "http://192.168.2.115/moodle"

func request(endpoint: String,
             parameters: [String: String],
             handler: @escaping (Result<Data, Error>) -> Void) {
    
    guard let url = URL(string: host + endpoint) else {
        handler(.failure(NetworkError.badURL))
        return
    }
    
    var items = [URLQueryItem]()
    var request = URLRequest(url: url)
    var componentes = URLComponents()
    
    for (name, value) in parameters {
        items.append(URLQueryItem(name: name, value: value))
    }
    componentes.queryItems = items
    
    let query = componentes.query?.data(using: .utf8)
    
    request.httpBody = query
    request.httpMethod = "POST"
    
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        DispatchQueue.main.async {
            guard let serverResponse = response as? HTTPURLResponse, (200...299).contains(serverResponse.statusCode) else {
                handler(.failure(NetworkError.badResponse))
                return
            }
            
            if let serverError = error {
                handler(.failure(serverError))
                return
            }
            
            if let serverData = data {
                handler(.success(serverData))
                return
            }
        }
    }
    
    task.resume()
}
}

My getCoursesList which returns an array of Courses

func getCoursesList() -> [Course] {
let networkRequest = NetworkRequest()
let token = UserDefaults.standard.string(forKey: "Token")!
var coursesList = [Course]()

networkRequest.request(endpoint: "/webservice/rest/server.php", parameters: ["wstoken": token, "wsfunction": "core_course_get_courses", "moodlewsrestformat": "json"]) { (result) in
    
    switch result {
    case .success:
        do {
            if let decode = try? JSONDecoder().decode([Course].self, from: result.get()) {
                coursesList = decode
            } else {
                let error = try JSONDecoder().decode(ErrorResponse.self, from: result.get())
                print(error)
            }
        }
        catch {
            print(error.localizedDescription)
        }
    case .failure(let error):
        print(error.localizedDescription)
    }
}

return coursesList
}

My problem is when I call te getCoursesList anywhere else, the result is em empty array. If printed inside the switch block, the expected result is decoded. How can I fix this?

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • There is nothing to fix and this has nothing to do with dispatch queues. Networking is asynchronous, that’s all. Inside the switch block is the only code you’ve got that occurs _after_ the networking finishes. It’s that simple. – matt Apr 14 '21 at 23:57
  • You can’t return `coursesList` because the network request runs asynchronously (i.e. the data is not received until _after_ you return from `getCoursesList`). So, don’t attempt to `return` the list, but rather give `getCoursesList` a `@escaping` “completion handler” closure that you call when the request finishes (just like you did in `request` method). I bet if you search for “Swift closure network request”, you’ll find plenty of examples, because this is very common issue for Swift developers new to asynchronous patterns. – Rob Apr 15 '21 at 00:08
  • I have a series of three articles that teaches you all about this: start at http://www.programmingios.net/what-asynchronous-means/ and just keep going. – matt Apr 15 '21 at 00:17

1 Answers1

0

As mentioned in the comments, you are returning coursesList before the asynchronous code can complete. The method executes practically instantaneously, and doesn't wait around for the async block to complete. So instead of returning the courses from the function, you can provide a completion block and pass back the response from there.

Here is a rough implementation. It may not compile so you may need to make some tweaks, especially with the error handling. I've also gone ahead and incorporated the very handy Result type, which consolidates the success and failure cases into one value. You can read more about this here, for example.

func getCoursesList(completion: @escaping () -> Result<[Course], Error>) {
    let networkRequest = NetworkRequest()
    let token = UserDefaults.standard.string(forKey: "Token")!
    var coursesList = [Course]()

    networkRequest.request(endpoint: "/webservice/rest/server.php", parameters: ["wstoken": token, "wsfunction": "core_course_get_courses", "moodlewsrestformat": "json"]) { (result) in

    switch result {
    case .success:
        do {
            if let courses = try? JSONDecoder().decode([Course].self, from: result.get()) {
                completion(.success(courses))
            } else {
                let error = try JSONDecoder().decode(ErrorResponse.self, from: result.get())
                print(error) // copied this from your code but I don't think this would ever actually fire
            }
        }
        catch {
            print(error.localizedDescription)
            completion(.failure(error))
        }
    case .failure(let error):
        print(error.localizedDescription)
        completion(.failure(error))
    }
}

Calling this method might look like this:

getCoursesList(
    completion: { result in
        switch result {
        case .success(let courses):
            // do something with your courses (make sure it's on the main queue if you're updating UI)
        case .failure(let error):
             // e.g. alert the user something went wrong (+ same point about main queue)
        }
    }
)
shim
  • 9,289
  • 12
  • 69
  • 108