Search code examples
swiftswiftuiopenweathermapurlsessionjsondecoder

How to get the 5 days forecast from OpenWeatherMap using Swift Ui


I´m using OpenWeatherMap api (https://openweathermap.org/forecast5#5days) to make a WeatherApp using swift, i already got the current weather data, but now i want to get the 5 days forecast, how could i do it if i only want to get the list atribute and the list atribute nested to ResponseApi. Please help.

I already tried to make it work, but i failed, makeDataRequestForecast its the function that retrieves the data from the api.

WeatherService

import CoreLocation
import Foundation


public final class WeatherService: NSObject {
    
    public let locationManager = CLLocationManager()
    private let API_KEY = "<api-key>"
    public var completionHandler: ((Weather) -> Void)?
    public var completionHandler2: ((Forecast) -> Void)?

    public override init(){
        super.init()
        locationManager.delegate = self
    }
    
    public func loadWeatherData(_ completionHandler: @escaping((Weather)->Void)) {
        self.completionHandler = completionHandler
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
    
    public func loadForecastData(_ completionHandler: @escaping((Forecast)->Void)) {
        self.completionHandler2 = completionHandler
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
    //api.openweathermap.org/data/2.5/forecast?lat=44.34&lon=10.99&appid={API key}
    
    
    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 {return}
        
            
        
            if let response = try? JSONDecoder().decode(ForecastApi.self, from: data) {
                let weatherList = response.list.map { Weather(response: $0) }
                let forecast = Forecast(list: weatherList)
                print(response)
                self.completionHandler2?(forecast)
                print(response)

            }else{
                print("error")
            }
        }.resume()
    }
    
    private func makeDataRequest(forCoordinates coordinates: CLLocationCoordinate2D){
        guard let urlString = "https://api.openweathermap.org/data/2.5/weather?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 {return}
        
        
        if let response = try? JSONDecoder().decode(ResponseApi.self, from: data){
            let weather = Weather(response: response)
            self.completionHandler?(weather)
        }else{
            print("error")
        }
        }.resume()
    }
}

extension WeatherService: CLLocationManagerDelegate{
    public func locationManager(
        _ manager: CLLocationManager,
        didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.first else {return}
            print("Location \(location.coordinate)")
            makeDataRequest(forCoordinates: location.coordinate)
            makeDataRequestForecast(forCoordinates: location.coordinate)
            
    }
    
    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Error: \(error.localizedDescription)")
    }
}

struct ForecastApi:Decodable{
    let list:[ResponseApi]
}

struct ResponseApi:Decodable{
    let name:String
    let timezone:Int
    let wind:Wind
    let clouds:Clouds
    let main: MainApi
    let weather:[WeatherApi]
}

struct Wind:Decodable{
    let speed:Double
}
struct Clouds:Decodable{
    let all:Double
}
struct MainApi:Decodable{
    let temp: Double
    let humidity:Int
}

struct WeatherApi : Decodable{
    let description:String
    let icon:String
    
    enum CodingKeys: String,CodingKey{
        case description
        case icon = "main"
        
    }
}



Weather

import Foundation

public struct Forecast{
    var lista:[Weather]
    
    init(list: [Weather]) {
        lista = list
    }
}

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 ?? ""
    }
}

func formatSecondsToHours(seconds: Int) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "h:mm a"
    
    let date = Date(timeIntervalSince1970: TimeInterval(seconds))
    let formattedString = formatter.string(from: date)
    
    return formattedString
}
func convertToDate(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"
    
    let currentDateTime = Date()
    let formattedDateTime = dateFormatter.string(from: currentDateTime)
    
    return formattedDateTime.capitalized
}

WeatherViewModel


import Foundation

private let defaultIcon = "❌"
private let iconMap = [
    "Drizzle" : "🌦️",
    "ThunderStorm" : "⛈️",
    "Snow" : "❄️",
    "Rain" : "🌧️",
    "Clear" : "☀️",
    "Clouds" : "☁️"
]


class WeatherViewModel:ObservableObject{
    @Published var cityname : String = "City Name"
    @Published var timezone : String = "00:00 __"
    @Published var date : String = "00/00/00"
    @Published var cloudsProb:String = "0 %"
    @Published var humidity:String = "0 %"
    @Published var windSpeed:String = "0 km/h"
    @Published var temperature : String = "__"
    @Published var weatherDescription : String = "__"
    @Published var weatherIcon : String = defaultIcon
    
    public let weatherService: WeatherService
    
    init(weatherService:WeatherService){
        self.weatherService = weatherService
    }
    
    func refresh(){
        weatherService.loadWeatherData{weather in
            DispatchQueue.main.async {
                self.cityname = weather.city
                self.timezone = weather.timezone
                self.date = weather.date
                self.cloudsProb = "\(weather.clouds) %"
                self.windSpeed = "\(weather.wind) km/h"
                self.humidity = "\(weather.humidity) %"
                self.temperature = "\(weather.temperature)°C"
                self.weatherDescription = weather.description.capitalized
                self.weatherIcon = iconMap[weather.icon] ?? defaultIcon
            }
        }
    }
}

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
                    let weatherViewModel = WeatherViewModel(weatherService: self.weatherService)
                    self.lista.append(weatherViewModel)
                }
            }
    }
}


Solution

  • You cannot retrieve the 5 days forecast data from the api because the model structs you have do not match the json data you get from the server. Paste your url into your browser, eg:

        https://api.openweathermap.org/data/2.5/forecast?lat=44.34&lon=10.99&appid={API key}
    

    then copy all of the json data into https://app.quicktype.io/, and all the correct struct models will be created for you.

    Adjust them to your purpose, eg, add Identifiable and all will work as it does for me in my tests.

    Also consult the docs https://openweathermap.org/forecast5#5days, to determine which properties are optionals, and add ? if required.

    Importantly, as mentioned, use do/try/catch around your decoding, and print the full error in the catch.

    For example:

     struct ForecastApi: Codable {
         let cod: String
         let message, cnt: Int
         let list: [ListResponse]
         let city: City
     }
    
     struct City: Codable {
         let id: Int
         let name: String
         let coord: Coord
         let country: String
         let population, timezone, sunrise, sunset: Int
     }
    
     struct Coord: Codable {
         let lat, lon: Double
     }
    
     struct ListResponse: Identifiable, Codable {
         let id = UUID()
         let dt: Int
         let main: MainClass
         let weather: [Weather]
         let clouds: Clouds
         let wind: Wind
         let visibility: Int
         let pop: Double
         let sys: Sys
         let dtTxt: String
         let rain: Rain?
    
         enum CodingKeys: String, CodingKey {
             case dt, main, weather, clouds, wind, visibility, pop, sys, rain
             case dtTxt = "dt_txt"
         }
     }
    
     struct Clouds: Codable {
         let all: Int
     }
    
     struct MainClass: Codable {
         let temp, feelsLike, tempMin, tempMax: Double
         let pressure, seaLevel, grndLevel, humidity: Int
         let tempKf: Double
    
         enum CodingKeys: String, CodingKey {
             case temp
             case feelsLike = "feels_like"
             case tempMin = "temp_min"
             case tempMax = "temp_max"
             case pressure
             case seaLevel = "sea_level"
             case grndLevel = "grnd_level"
             case humidity
             case tempKf = "temp_kf"
         }
     }
    
     struct Rain: Codable {
         let the3H: Double
    
         enum CodingKeys: String, CodingKey {
             case the3H = "3h"
         }
     }
    
     struct Sys: Codable {
         let pod: Pod
     }
    
     enum Pod: String, Codable {
         case d = "d"
         case n = "n"
     }
    
    struct Weather: Codable {
        let id: Int
        let main: String
        let description: String
        let icon: String
    }
    
     enum Description: String, Codable {
         case brokenClouds = "broken clouds"
         case fewClouds = "few clouds"
         case lightRain = "light rain"
         case moderateRain = "moderate rain"
         case overcastClouds = "overcast clouds"
         case scatteredClouds = "scattered clouds"
     }
    
     enum MainEnum: String, Codable {
         case clouds = "Clouds"
         case rain = "Rain"
     }
    
     struct Wind: Codable {
         let speed: Double
         let deg: Int
         let gust: Double
     }