iosswiftnsurlsession

Nil in OpenWeather API


I've just started to learn Swift and iOS and I have problem in my weather app. I'm trying to get weather data and print in console, but there is still nil and I no have idea what could be wrong. I tried to change the model, pasted all URL and is still nil. Sometimes app sends data and there is no answer, but at other times is answer or no sending, no answer. In console always nil...

Generated model class:

import Foundation

class CurrentWeather : NSObject, NSCoding{
    
    var base : String!
    var clouds : Cloud!
    var cod : Int!
    var coord : Coord!
    var dt : Int!
    var id : Int!
    var main : Main!
    var name : String!
    var sys : Sy!
    var visibility : Int!
    var weather : [Weather]!
    var wind : Wind!

    /**
     * Instantiate the instance using the passed dictionary values to set the properties values
     */
    init(fromDictionary dictionary: [String:Any]){
        base = dictionary["base"] as? String
        cod = dictionary["cod"] as? Int
        dt = dictionary["dt"] as? Int
        id = dictionary["id"] as? Int
        name = dictionary["name"] as? String
        visibility = dictionary["visibility"] as? Int
        if let cloudsData = dictionary["clouds"] as? [String:Any]{
            clouds = Cloud(fromDictionary: cloudsData)
        }
        if let coordData = dictionary["coord"] as? [String:Any]{
            coord = Coord(fromDictionary: coordData)
        }
        if let mainData = dictionary["main"] as? [String:Any]{
            main = Main(fromDictionary: mainData)
        }
        if let sysData = dictionary["sys"] as? [String:Any]{
            sys = Sy(fromDictionary: sysData)
        }
        if let windData = dictionary["wind"] as? [String:Any]{
            wind = Wind(fromDictionary: windData)
        }
        weather = [Weather]()
        if let weatherArray = dictionary["weather"] as? [[String:Any]]{
            for dic in weatherArray{
                let value = Weather(fromDictionary: dic)
                weather.append(value)
            }
        }
    }

    /**
     * Returns all the available property values in the form of [String:Any] object where the key is the approperiate json key and the value is the value of the corresponding property
     */
    func toDictionary() -> [String:Any]
    {
        var dictionary = [String:Any]()
        if base != nil{
            dictionary["base"] = base
        }
        if cod != nil{
            dictionary["cod"] = cod
        }
        if dt != nil{
            dictionary["dt"] = dt
        }
        if id != nil{
            dictionary["id"] = id
        }
        if name != nil{
            dictionary["name"] = name
        }
        if visibility != nil{
            dictionary["visibility"] = visibility
        }
        if clouds != nil{
            dictionary["clouds"] = clouds.toDictionary()
        }
        if coord != nil{
            dictionary["coord"] = coord.toDictionary()
        }
        if main != nil{
            dictionary["main"] = main.toDictionary()
        }
        if sys != nil{
            dictionary["sys"] = sys.toDictionary()
        }
        if wind != nil{
            dictionary["wind"] = wind.toDictionary()
        }
        if weather != nil{
            var dictionaryElements = [[String:Any]]()
            for weatherElement in weather {
                dictionaryElements.append(weatherElement.toDictionary())
            }
            dictionary["weather"] = dictionaryElements
        }
        return dictionary
    }

    /**
     * NSCoding required initializer.
     * Fills the data from the passed decoder
     */
    @objc required init(coder aDecoder: NSCoder)
    {
        base = aDecoder.decodeObject(forKey: "base") as? String
        clouds = aDecoder.decodeObject(forKey: "clouds") as? Cloud
        cod = aDecoder.decodeObject(forKey: "cod") as? Int
        coord = aDecoder.decodeObject(forKey: "coord") as? Coord
        dt = aDecoder.decodeObject(forKey: "dt") as? Int
        id = aDecoder.decodeObject(forKey: "id") as? Int
        main = aDecoder.decodeObject(forKey: "main") as? Main
        name = aDecoder.decodeObject(forKey: "name") as? String
        sys = aDecoder.decodeObject(forKey: "sys") as? Sy
        visibility = aDecoder.decodeObject(forKey: "visibility") as? Int
        weather = aDecoder.decodeObject(forKey: "weather") as? [Weather]
        wind = aDecoder.decodeObject(forKey: "wind") as? Wind
    }

    /**
     * NSCoding required method.
     * Encodes mode properties into the decoder
     */
    @objc func encode(with aCoder: NSCoder)
    {
        if base != nil{
            aCoder.encode(base, forKey: "base")
        }
        if clouds != nil{
            aCoder.encode(clouds, forKey: "clouds")
        }
        if cod != nil{
            aCoder.encode(cod, forKey: "cod")
        }
        if coord != nil{
            aCoder.encode(coord, forKey: "coord")
        }
        if dt != nil{
            aCoder.encode(dt, forKey: "dt")
        }
        if id != nil{
            aCoder.encode(id, forKey: "id")
        }
        if main != nil{
            aCoder.encode(main, forKey: "main")
        }
        if name != nil{
            aCoder.encode(name, forKey: "name")
        }
        if sys != nil{
            aCoder.encode(sys, forKey: "sys")
        }
        if visibility != nil{
            aCoder.encode(visibility, forKey: "visibility")
        }
        if weather != nil{
            aCoder.encode(weather, forKey: "weather")
        }
        if wind != nil{
            aCoder.encode(wind, forKey: "wind")
        }
    }
    
}

WeatherService:

import Foundation

class WeatherSerice {
    
    let weatherAPIKey: String
    let weatherBaseURL: URL?
    
    init(APIKey: String) {
        
        self.weatherAPIKey = APIKey
        weatherBaseURL = URL(string: "https://api.openweathermap.org/data/2.5/weather?")
    }
    
    func getCurrentWeather(city: String, completion: @escaping (CurrentWeather?) -> Void) {
        
        if let weatherURL = URL(string: "\(weatherBaseURL!)q=\(city)&appid=\(weatherAPIKey)") {
            
            let networkProcessor = NetworkProcessor(url: weatherURL)
            networkProcessor.downloadJSONFromURL({(jsonDictionary) in
                
            if let currentWeatherDictionary = jsonDictionary?["currently"] as?
                [String : Any] {
                let currentWeather = CurrentWeather(fromDictionary: currentWeatherDictionary)
                completion(currentWeather)
            } else {
                completion(nil)
                }
            })
        }
    }
}

Part of the UIViewController:

let weatherService = WeatherSerice(APIKey: "52e6ff60bba8613b4850e065dcd3d0ac")
weatherService.getCurrentWeather(city: "London") {
    (currentWeather) in
    
    print (currentWeather)
}

Solution

  • Please, please drop NSCoding in favor of Codable, you will get rid of all that ugly boilerplate code

    struct WeatherData : Decodable {
        let coord : Coordinate
        let cod, visibility, id : Int
        let name : String
        let base : String
        let weather : [Weather]
        let clouds: Clouds
        let sys : Sys
        let main : Main
        let wind : Wind
        let dt : Date
    }
    
    struct Coordinate : Decodable {
        let lat, lon : Double
    }
    
    struct Weather : Decodable {
        let id : Int
        let icon : String
        let main : MainEnum
        let description: String
    }
    
    struct Sys : Decodable {
        let type, id : Int
        let sunrise, sunset : Date
        let message : Double
        let country : String
    }
    
    struct Main : Decodable {
        let temp, tempMin, tempMax : Double
        let pressure, humidity : Int
    }
    
    struct Wind : Decodable {
        let speed : Double
        let deg : Int
        let gust : Double?
    }
    
    struct Clouds: Decodable {
        let all: Int
    }
    
    enum MainEnum: String, Decodable {
        case clear = "Clear"
        case clouds = "Clouds"
        case rain = "Rain"
    }
    

    As you can see the dates are decoded as Date and the main values in Weather are decoded as enum.


    I don't know the API NetworkProcessor so I replaced it with traditional URLSession. The added URLComponents provide proper URL encoding for example if the city contains space characters

    import Foundation
    
    class WeatherSerice {
    
        let weatherAPIKey: String
        let weatherBase = "https://api.openweathermap.org/data/2.5/weather"
    
        init(APIKey: String) {
    
            self.weatherAPIKey = APIKey
        }
    
        func getCurrentWeather(city: String, completion: @escaping (WeatherData?) -> Void) {
    
            var urlComponents = URLComponents(string: weatherBase)!
            let queryItems = [URLQueryItem(name: "q", value: city),
                             URLQueryItem(name: "appid", value: weatherAPIKey)]                     
            urlComponents.queryItems = queryItems
    
            if let weatherURL = urlComponents.url {
    
                let task = URLSession.shared.dataTask(with: weatherURL) { (data, response, error) in
    
                    if let error = error { print(error); completion(nil) }
    
                    do {
                        let decoder = JSONDecoder()
                        decoder.keyDecodingStrategy = .convertFromSnakeCase
                        decoder.dateDecodingStrategy = .secondsSince1970
                        let result = try decoder.decode(WeatherData.self, from: data!)
                        completion(result)  
                    } catch { 
                        print(error)
                        completion(nil)
                    }       
                }
                task.resume()
            }
        }
    }
    

    I recommend to use the new Result type in Swift 5 to return the error, too.

        func getCurrentWeather(city: String, completion: @escaping (Result<WeatherData,Error>) -> Void) {
    
            var urlComponents = URLComponents(string: weatherBase)!
            let queryItems = [URLQueryItem(name: "q", value: city),
                             URLQueryItem(name: "appid", value: weatherAPIKey)]                     
            urlComponents.queryItems = queryItems
    
            if let weatherURL = urlComponents.url {
    
                let task = URLSession.shared.dataTask(with: weatherURL) { (data, response, error) in
    
                    if let error = error { completion(.failure(error)) }
    
                    do {
                        let decoder = JSONDecoder()
                        decoder.keyDecodingStrategy = .convertFromSnakeCase
                        decoder.dateDecodingStrategy = .secondsSince1970
                        let result = try decoder.decode(WeatherData.self, from: data!)
                        completion(.success(result))
                    } catch { 
                        completion(.failure(error))
                    }       
                }
                task.resume()
            }
    

    and use it

    weatherService.getCurrentWeather(city: "London") { result in
        switch result {
        case .success(let result): print(result)
        case .failure(let error): print(error)
        }
    }