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