Home > other >  URLSession.shared.dataTask Code Block Not Running
URLSession.shared.dataTask Code Block Not Running

Time:03-11

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.....")

CodePudding user response:

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:

  1. 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:

  1. You have some other network request whose completion handler is blocked/deadlocked, thereby preventing anything else from running on the URLSession dedicated, serial, queue.

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

CodePudding user response:

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,
    apiKey: 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, apiKey: apiKey) { [weak self] result in
    switch result {
    case .failure(let error): print(error)
    case .success(let elements): self?.model = elements
    }
}

This has too 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.

  • Related