Home > database >  Does Swift task run first or print() first when I tap my UIButton?
Does Swift task run first or print() first when I tap my UIButton?

Time:12-19

I am trying to understand what is going on in my code here.

I have a simple API call to open weahter API and that whenever the user taps the UIButton, it should call the api and get the data back from open weather.

Everything works as intended however, when I have my UIButton pressed, the print statement executed first before the Task closure. I'm trying to understand the race condition here

This is my code in viewController:

@IBAction func callAPIButton(_ sender: UIButton)  {

        Task {
            let weatherData =  await weatherManager.fetchWeather(cityName: "Seattle")
        }
        
    }


Here's the code for fetching the API:

struct WeatherManager{
    let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=someAPIKeyHere"
    
    func fetchWeather(cityName: String)  -> WeatherModel? {
        let urlString = "\(weatherURL)&q=\(cityName)"

            let requestResult =  performRequest(urlString: urlString)
            return requestResult
    }
    
    func performRequest(urlString: String)  -> WeatherModel? {
        var weatherResult : WeatherModel? = nil
        
        if let url = URL(string: urlString){
            
            let session = URLSession(configuration: .default)
            
            let task =  session.dataTask(with: url, completionHandler: {
                (data, response, error) in
                
                if error != nil {
                    return
                }
                if let safeData = data {
                    weatherResult = parseJSON(weatherData: safeData)
                }
            })
            task.resume()
        }
        return weatherResult
    }
    
    func parseJSON(weatherData: Data) -> WeatherModel?{
        let decoder  = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherResponse.self, from: weatherData)
            print("this is in decodedData: \(decodedData)")
            let temp = decodedData.main.temp
            let name = decodedData.name
            let weather = WeatherModel(conditionId:300, cityName: name, temperature: temp)
            return weather
        } catch {
            print("Something is wrong here: "    error.localizedDescription)
        }
        return nil
    }

}

Here's my Model:

struct WeatherModel{
    let conditionId: Int
    let cityName: String
    let temperature: Double
    
    var temperatureString: String{
        return String(format: "%.1f", temperature)
    }
    
    var conditionName: String {
            switch conditionId {
            case 200...232:
                return "cloud.bolt"
            case 300...321:
                return "cloud.drizzle"
            case 500...531:
                return "cloud.rain"
            case 600...622:
                return "cloud.snow"
            case 701...781:
                return "cloud.fog"
            case 800:
                return "sun.max"
            case 801...804:
                return "cloud.bolt"
            default:
                return "cloud"
            }
        }
    
}

Desired result:

This is in weatherData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e 09, sunset: 1.6713243e 09))

This is what I am getting instead:

This is in weatherData: nil
this is in decodedData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e 09, sunset: 1.6713243e 09))

Thank you in advance

CodePudding user response:

Everything works as intended

No, it doesn't. I don't know why you claim such a thing; your code isn't working at all.

The problem is that you are trying to return weatherResult from performRequest. But performRequest gets its weatherResult value asynchronously, so this attempt is doomed to failure; you will always be returning nil, because the return weatherResult happens before session.dataTask ever even starts to find out what weatherResult is.

CodePudding user response:

You cannot just synchronously return the results of an asynchronous request. You have two basic options for asynchronous requests.

  1. Use the older “completion handler” pattern with Result types:

    struct WeatherManager {
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
        let appId = "someAPIKeyHere"
    
        func fetchWeather(
            cityName: String,
            completion: @escaping (Result<WeatherModel, Error>) -> Void
        ) {
            guard var components = URLComponents(string: weatherURL) else {
                completion(.failure(URLError(.badURL)))
                return
            }
            components.queryItems = [
                URLQueryItem(name: "appid", value: appId),
                URLQueryItem(name: "q", value: cityName)
            ]
    
            guard let url = components.url else {
                completion(.failure(URLError(.badURL)))
                return
            }
    
            performRequest(url: url, completion: completion)
        }
    
        func performRequest(
            url: URL,
            queue: DispatchQueue = .main,
            completion: @escaping (Result<WeatherModel, Error>) -> Void
        ) {
            let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
    
            let task = session.dataTask(with: url) { data, response, error in
                guard
                    error == nil,
                    let data = data,
                    let response = response as? HTTPURLResponse,
                    200 ..< 300 ~= response.statusCode
                else {
                    queue.async { completion(.failure(error ?? URLError(.badServerResponse))) }
                    return
                }
    
                do {
                    let weatherResult = try parseJSON(weatherData: data)
                    queue.async { completion(.success(weatherResult)) }
                } catch {
                    queue.async { completion(.failure(error)) }
                }
            }
            task.resume()
        }
    
        func parseJSON(weatherData: Data) throws -> WeatherModel {
            let decoder = JSONDecoder()
            let response = try decoder.decode(WeatherResponse.self, from: weatherData)
            print("this is in decodedData: \(response)")
            return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
        }
    }
    

    Then, rather than:

    let weather = weatherManager.fetchWeather(cityName: …)
    

    You would

    weatherManager.fetchWeather(cityName: …) { result in
        switch result {
        case .failure(let error):
            print(error)
    
        case .success(let weather):
            // do something with the `weather` object here
        }
    }
    
    // note, do not do anything with `weather` here, because the above
    // runs asynchronously (i.e., later).
    
  2. Use the newer async-await pattern of Swift concurrency:

    struct WeatherManager {
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
        let appId = "someAPIKeyHere"
    
        func fetchWeather(cityName: String) async throws -> WeatherModel {
            guard var components = URLComponents(string: weatherURL) else {
                throw URLError(.badURL)
            }
            components.queryItems = [
                URLQueryItem(name: "appid", value: appId),
                URLQueryItem(name: "q", value: cityName)
            ]
    
            guard let url = components.url else {
                throw URLError(.badURL)
            }
    
            return try await performRequest(url: url)
        }
    
        func performRequest(url: URL) async throws -> WeatherModel {
            let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
    
            let (data, response) = try await session.data(from: url)
            guard
                let response = response as? HTTPURLResponse,
                200 ..< 300 ~= response.statusCode
            else {
                throw URLError(.badServerResponse)
            }
    
            return try parseJSON(weatherData: data)
        }
    
        func parseJSON(weatherData: Data) throws -> WeatherModel {
            let decoder = JSONDecoder()
            do {
                let response = try decoder.decode(WeatherResponse.self, from: weatherData)
                print("this is in decodedData: \(response)")
                return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
            } catch {
                print("Something is wrong here: "    error.localizedDescription)
                throw error
            }
        }
    }
    

    And then you can do things like:

    Task {
        do {
            let weather = try await weatherManager.fetchWeather(cityName: …)
            // do something with `weather` here
        } catch {
            print(error)
        }
    }
    

Note, a few changes in the above unrelated to the asynchronous nature of your request:

  • Avoid creating URLSession instances. If you do, you need to remember to invalidate them. Instead, it is much easier to use URLSession.shared, eliminating this annoyance.

  • Avoid building URLs with string interpolation. Use URLComponents to build safe URLs (e.g., ones that can handle city names like “San Francisco”, with spaces in their names).

  • Related