[Solved]
I was not treating reverseGeocode as an asynchronous function, leading to timing issues and data not populating correctly. Using async await solved the issue.
Solution:
func getCurrentWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> WeatherReport? {
do {
guard let url = URL(string: "https://api.openweathermap.org/data/3.0/onecall?lat=\(latitude)&lon=\(longitude)&exclude=current,minutely,hourly, alerts&appid=19054ace743dfdfd9&units=imperial") else { throw NetworkError.badURL}
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
let decodedData = try JSONDecoder().decode(WeatherReport.self, from: data)
let decodedDataWithCity = try await decodedData.reverseGeocode()
return decodedDataWithCity
} catch NetworkError.badURL {
print("Error creating URL")
} catch NetworkError.badResponse {
print("Didn't get a valid response")
} catch NetworkError.badStatus {
print("Didn't get a 2xx status code from response")
} catch {
print("An error occurred downloading the data")
}
return nil
}
func reverseGeocode() async throws -> WeatherReport? {
let geocoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: lon)
var report: WeatherReport?
do {
let placemarks = try await geocoder.reverseGeocodeLocation(location)
let placemark = placemarks.first
report = WeatherReport(lat: lat, lon: lon, city: placemark?.locality, timezone: timezone, timezoneOffset: timezoneOffset, daily: daily)
} catch {
print("Error: \(error.localizedDescription)")
}
return report
}
func getCurrentWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> WeatherReport? {
do {
guard let url = URL(string: "https://api.openweathermap.org/data/3.0/onecall?lat=\(latitude)&lon=\(longitude)&exclude=current,minutely,hourly, alerts&appid=19054ace749133743dfdfd9&units=imperial") else { throw NetworkError.badURL}
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
let decodedData = try JSONDecoder().decode(WeatherReport.self, from: data)
let decodedDataWithCity = decodedData.reverseGeocode(latitude: decodedData.lat, longitude: decodedData.lon)
return decodedDataWithCity
} catch NetworkError.badURL {
print("Error creating URL")
} catch NetworkError.badResponse {
print("Didn't get a valid response")
} catch NetworkError.badStatus {
print("Didn't get a 2xx status code from response")
} catch {
print("An error occurred downloading the data")
}
return nil
}
[Original Post]
I'm building a simple weather app that displays:
- User's city name (having trouble with this part)
- Current temp
- Daily min/max temp
- Wind speed
- Humidity
The OpenWeatherMap API provides data for everything except city name, and I am having trouble figuring out the logic flow to obtain it.
My current (failed) approach is:
- Create a model called
WeatherReportcontaining all properties of a weather report.
struct WeatherReport: Codable {
var lat, lon: Double
var city: String?
let timezone: String
let timezoneOffset: Int
let daily: [Daily]
- I've added
cityhere as optional because the API called to populate these properties will not return a city name. citya computed property that's determined through reverse geocoding (provide latitude, longitude and return location name).
- Add .task to download data from API and decode into model (this is working).
func getCurrentWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> WeatherReport? {
do {
guard let url = URL(string: "https://api.openweathermap.org/data/3.0/onecall?lat=\(latitude)&lon=\(longitude)&exclude=current,minutely,hourly, alerts&appid=19054ace7493dfdfd9&units=imperial") else { throw NetworkError.badURL}
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
let decodedData = try JSONDecoder().decode(WeatherReport.self, from: data)
return decodedData
} catch NetworkError.badURL {
print("Error creating URL")
} catch NetworkError.badResponse {
print("Didn't get a valid response")
} catch NetworkError.badStatus {
print("Didn't get a 2xx status code from response")
} catch {
print("An error occurred downloading the data")
}
return nil
}
(Here's where I'm having all the trouble)
- Populate the
cityproperty now that we have lat/lon data available.
Attempt #1
- Create
reverseGeocodefunction that takes latitute/longitude (CLLocationDegrees) and returns a city name (String). - Use
didSetproperty observer on lat/lon properties to callreverseGeocodefunction to computecity
Failed because 'var' declarations with multiple variables cannot have explicit getters/setters
var lat, lon: Double {
didSet {
self.city = reverseGeocode(latitude: lat, longitude: lon)
}
}
func reverseGeocode(latitude: CLLocationDegrees, longitude: CLLocationDegrees) -> String {
let geocoder = CLGeocoder()
let location = CLLocation(latitude: latitude, longitude: longitude)
var cityName: String
geocoder.reverseGeocodeLocation(location) { placemarks, error in
print("in geocoder.reverseGeocodeLocation function")
// ensure no error
guard error == nil else { return }
// ensure there are placemarks
if let placemarks = placemarks,
let placemark = placemarks.first {
cityName = placemark.locality ?? "Current location"
}
}
return cityName
}
Attempt #2
- Change the
reverseGeocodemethod to return aWeatherReportrather than aString - Within the original API call, once a
WeatherReport(excluding city name) has been successfully decoded - we callreverseGeocodemethod to generate a newWeatherReportthat does include city name.
Failed because it's hung up on the async task of obtaining the WeatherReport to generate the view. Originally thought the reverseGeocode method wasn't working, but added some print statements inside it that confirms it is.
The view is stuck on the loading spinner while waiting for the WeatherReport to come in.
@EnvironmentObject private var locationManager: LocationManager
private let weatherManager = WeatherManager()
@State var weatherReport: WeatherReport?
var body: some View {
ZStack {
// if location exists...
if let location = locationManager.location {
// ..and weather report exists
if let weatherReport = weatherReport {
// show WeatherView
WeatherView(weatherReport: weatherReport)
} else {
// show loading spinner
LoadingView()
.task {
do {
// obtain weather report
weatherReport = try await weatherManager.getCurrentWeather(latitude: location.latitude, longitude: location.longitude)
} catch {
print("Unable to get weather info - Error: \(error.localizedDescription)")
}
}
}
func getCurrentWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> WeatherReport? {
do {
guard let url = URL(string: "https://api.openweathermap.org/data/3.0/onecall?lat=\(latitude)&lon=\(longitude)&exclude=current,minutely,hourly, alerts&appid=19054ace749743dfdfd9&units=imperial") else { throw NetworkError.badURL}
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
let decodedData = try JSONDecoder().decode(WeatherReport.self, from: data)
let decodedDataWithCity = decodedData.reverseGeocode(latitude: decodedData.lat, longitude: decodedData.lon)
return decodedDataWithCity
} catch NetworkError.badURL {
print("Error creating URL")
} catch NetworkError.badResponse {
print("Didn't get a valid response")
} catch NetworkError.badStatus {
print("Didn't get a 2xx status code from response")
} catch {
print("An error occurred downloading the data")
}
return nil
}
func reverseGeocode(latitude: CLLocationDegrees, longitude: CLLocationDegrees) -> WeatherReport {
let geocoder = CLGeocoder()
let location = CLLocation(latitude: latitude, longitude: longitude)
var report: WeatherReport
geocoder.reverseGeocodeLocation(location) { placemarks, error in
print("in geocoder.reverseGeocodeLocation function")
// ensure no error
guard error == nil else { return }
// ensure there are placemarks
if let placemarks = placemarks,
let placemark = placemarks.first {
report = WeatherReport(lat: lat, lon: lon, city: placemark.locality, timezone: timezone, timezoneOffset: timezoneOffset, daily: daily)
print("report: \(report)")
}
}
return report
}
For additional context here's the view I'm trying to build. Any help would be greatly appreciated, thanks!

You could make a
CityView, e.g.