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)
}
}
}
}
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
}