如何在SwiftUI中通过OpenWeatherMap获取5天天气预报?
解决OpenWeatherMap 5天天气预报API解析问题
核心问题分析
你的代码主要在API数据模型匹配、数据初始化逻辑和ViewModel数据绑定三个环节出错,导致5天预报数据无法正确获取和展示。以下是具体修复步骤:
1. 修正API数据模型结构
OpenWeatherMap的5天预报API返回的list数组条目结构,和当前天气API(/weather)完全不同,不能直接用ResponseApi解析。需要新增对应预报条目的模型:
// 替换WeatherService中的Decodable结构体 struct ForecastApi: Decodable { let city: ForecastCity // 存储城市名称、时区等全局信息 let list: [ForecastItem] // 每个时间点的预报数据 } struct ForecastCity: Decodable { let name: String let timezone: Int } struct ForecastItem: Decodable { let dt: Int // 预报时间戳 let wind: Wind let clouds: Clouds let main: MainApi let weather: [WeatherApi] } // 修正WeatherApi的字段映射:之前把icon和main的key搞反了 struct WeatherApi : Decodable{ let description: String let main: String // 对应API的main字段(天气主状态) let icon: String // 对应API的icon字段(图标代码) enum CodingKeys: String, CodingKey{ case description case main case icon } }
2. 扩展Weather结构体的初始化方法
预报条目本身不包含城市名称和时区,需要新增接收预报数据+城市信息的初始化逻辑:
// 在Weather结构体中添加新的初始化方法 public struct Weather{ let city:String let timezone: String let date: String let clouds:String let wind:String let humidity:String let temperature:String let description:String let icon:String // 保留原有的当前天气初始化方法 init(response:ResponseApi){ city = response.name timezone = formatSecondsToHours(seconds: response.timezone) date = convertToDate(timeZoneOffset: response.timezone) clouds = "\(Int(response.clouds.all)) " wind = "\(response.wind.speed)" humidity = "\(response.main.humidity)" temperature = "\(Int(response.main.temp))" description = response.weather.first?.description ?? "" icon = response.weather.first?.icon ?? "" } // 新增预报数据的初始化方法 init(forecastItem: ForecastItem, cityName: String, timezoneOffset: Int) { city = cityName timezone = formatSecondsToHours(seconds: timezoneOffset) date = convertToDate(timestamp: forecastItem.dt, timeZoneOffset: timezoneOffset) clouds = "\(Int(forecastItem.clouds.all))" wind = "\(forecastItem.wind.speed)" humidity = "\(forecastItem.main.humidity)" temperature = "\(Int(forecastItem.main.temp))" description = forecastItem.weather.first?.description ?? "" icon = forecastItem.weather.first?.icon ?? "" } } // 新增根据时间戳生成预报日期的方法 func convertToDate(timestamp: Int, timeZoneOffset: Int) -> String { let timezone = TimeZone(secondsFromGMT: timeZoneOffset) let dateFormatter = DateFormatter() dateFormatter.timeZone = timezone dateFormatter.locale = Locale(identifier: "en_US") dateFormatter.dateFormat = "EEEE, MMMM d yyyy HH:mm" let forecastDate = Date(timeIntervalSince1970: TimeInterval(timestamp)) return dateFormatter.string(from: forecastDate).capitalized }
3. 修复makeDataRequestForecast解析逻辑
现在用新模型解析API响应,并正确生成Weather数组:
private func makeDataRequestForecast(forCoordinates coordinates: CLLocationCoordinate2D){ guard let urlString = "https://api.openweathermap.org/data/2.5/forecast?lat=\(coordinates.latitude)&lon=\(coordinates.longitude)&appid=\(API_KEY)&units=metric".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} guard let url = URL(string: urlString) else {return} URLSession.shared.dataTask(with: url){ data, response, error in guard error == nil, let data = data else { print("网络请求错误: \(error?.localizedDescription ?? "未知错误")") return } do { let forecastResponse = try JSONDecoder().decode(ForecastApi.self, from: data) // 遍历预报条目,生成Weather对象 let weatherList = forecastResponse.list.map { item in Weather(forecastItem: item, cityName: forecastResponse.city.name, timezoneOffset: forecastResponse.city.timezone) } let forecast = Forecast(list: weatherList) self.completionHandler2?(forecast) } catch { print("解码错误: \(error.localizedDescription)") print(error) // 打印详细错误,方便调试 } }.resume() }
4. 修正ForecastViewModel的数据绑定逻辑
之前的ViewModel只是添加空的实例,没有用获取到的预报数据填充,现在修改如下:
class ForecastViewModel:ObservableObject{ @Published var lista: [WeatherViewModel] = [] public let weatherService: WeatherService init(weatherService:WeatherService){ self.weatherService = weatherService } func refreshForecast(){ weatherService.loadForecastData { forecast in DispatchQueue.main.async { [self] in // 清空旧数据,遍历预报数据生成ViewModel self.lista.removeAll() forecast.lista.forEach { weather in let vm = WeatherViewModel(weatherService: self.weatherService) vm.cityname = weather.city vm.timezone = weather.timezone vm.date = weather.date vm.cloudsProb = "\(weather.clouds) %" vm.windSpeed = "\(weather.wind) km/h" vm.humidity = "\(weather.humidity) %" vm.temperature = "\(weather.temperature)°C" vm.weatherDescription = weather.description.capitalized vm.weatherIcon = iconMap[weather.icon] ?? defaultIcon self.lista.append(vm) } } } } }
5. 小细节优化
- 统一
Forecast结构体的命名:
public struct Forecast{ let lista:[Weather] init(list: [Weather]) { self.lista = list } }
- 避免重复请求:在
locationManager(_:didUpdateLocations:)中,可以根据当前调用的是loadWeatherData还是loadForecastData,只触发对应的API请求,避免每次更新位置都同时请求两个接口。
内容的提问来源于stack exchange,提问作者Edgar Narv Mar
相关产品推荐
相关产品推荐

