You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 23:47:03