Search code examples
swiftuicombine

OpenWeather data not returning using SwiftUI and Combine


I'm not sure why I'm not getting any data returned back. I know the API key and URL is working properly. Below is the Struct and my class I'm using. I also included what I'm doing in my SwiftUI file.

In the response I can see that I am getting back a 200. I've tried a few different ways to mapping the data to my view, but so far no luck.

// MARK: - Welcome
struct WeatherDataModel: Codable, Identifiable {
    let id = UUID()
    let lat, lon: Double
    let timezone: String
    let current: Current
    let daily: [Daily]

    enum CodingKeys: String, CodingKey {
        case lat, lon, timezone
        case current, daily
    }
}

// MARK: - Current
struct Current: Codable {
    let dt, sunrise, sunset: Int
    let temp, feelsLike: Double
    let pressure, humidity: Int
    let dewPoint: Double
    let uvi, clouds, visibility: Int
    let windSpeed: Double
    let windDeg: Int
    let weather: [Weather]

    enum CodingKeys: String, CodingKey {
        case dt, sunrise, sunset, temp
        case feelsLike = "feels_like"
        case pressure, humidity
        case dewPoint = "dew_point"
        case uvi, clouds, visibility
        case windSpeed = "wind_speed"
        case windDeg = "wind_deg"
        case weather
    }
}

// MARK: - Weather
struct Weather: Codable {
    let id: Int
    let main, weatherDescription, icon: String

    enum CodingKeys: String, CodingKey {
        case id, main
        case weatherDescription = "description"
        case icon
    }
}

// MARK: - Daily
struct Daily: Codable {
    let dt, sunrise, sunset, moonrise: Int
    let moonset: Int
    let moonPhase: Double
    let temp: Temp
    let feelsLike: FeelsLike
    let pressure, humidity: Int
    let dewPoint, windSpeed: Double
    let windDeg: Int
    let windGust: Double
    let weather: [Weather]
    let clouds: Int
    let pop: Double
    let rain: Double?
    let uvi: Double

    enum CodingKeys: String, CodingKey {
        case dt, sunrise, sunset, moonrise, moonset
        case moonPhase = "moon_phase"
        case temp
        case feelsLike = "feels_like"
        case pressure, humidity
        case dewPoint = "dew_point"
        case windSpeed = "wind_speed"
        case windDeg = "wind_deg"
        case windGust = "wind_gust"
        case weather, clouds, pop, rain, uvi
    }
}

// MARK: - FeelsLike
struct FeelsLike: Codable {
    let day, night, eve, morn: Double
}

// MARK: - Temp
struct Temp: Codable {
    let day, min, max, night: Double
    let eve, morn: Double
}

typealias weatherData = [WeatherDataModel]

class DownloadWeatherData: ObservableObject {

    @Published var weatherdata: [WeatherDataModel] = []
    var weatherCancellabes = Set<AnyCancellable>()

    init() {
        print("loading weather init")

        getWeather(weatherUrl: "<my url>")
    }

    func getWeather(weatherUrl: String) {

        guard let weatherUrl = URL(string: weatherUrl) else { return }

        URLSession.shared.dataTaskPublisher(for: weatherUrl)
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .tryMap { (data, response) -> Data in
                print(response)
                guard
                    let response = response as? HTTPURLResponse,
                      response.statusCode >= 200 && response.statusCode < 300 else {
                    throw URLError(.badServerResponse)
                }
                print("data \(data)")
                return data
            }
            .decode(type: [WeatherDataModel].self, decoder: JSONDecoder())
            .sink { (completion) in
            } receiveValue: { [weak self] (returnedWeatherData) in
                self?.weatherdata = returnedWeatherData
                print("returnedWeatherData \(returnedWeatherData)")
            }
            .store(in: &weatherCancellabes)
    }
}


struct WeatherView: View {

    @StateObject var weatherData = DownloadWeatherData()

    var body: some View {
        VStack {
            ForEach(weatherData.weatherdata) { day in
                Text(day.timezone)
            }
        }
    }
}

The error I'm getting here is No exact matches in call to initializer


Solution

  • Tips:

    1. you don't need to define CodingKeys enum, just set .keyDecodingStrategy = .convertFromSnakeCase for your JSON decoder; all underbar key, like wind_gust, will be automatically convert to camelCase, windGust for example.
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    
    1. Use optional instead of non-optional model's properties to make sure any missing return keys will not fail your decoder. If the property is 100% guarantee from your backend dev, you can remove optional later.

    2. Check datatype carefully of the response string from server, sometimes they return String but we use model with Double or Int property and also check Array or Dictionary; as my experience, most of time the root object is Dictionary so just check [WeatherDataModel] again.