I am building a small swift weather app using the openweatherAPI and I am running into some issues trying to parse the JSON. I have used the following function to parse the get and parse the json.
Below is my weather data struct:
struct WeatherData: Codable {
let coord: Coord
let weather: [Weather]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let id: Int
let name: String
let cod: Int
}
struct Clouds: Codable {
let all: Int
}
struct Coord: Codable {
let lon, lat: Double
}
struct Main: Codable {
let temp: Double
let pressure, humidity: Int
let tempMin, tempMax: Double
enum CodingKeys: String, CodingKey {
case temp, pressure, humidity
case tempMin = "temp_min"
case tempMax = "temp_max"
}
}
struct Sys: Codable {
let type, id: Int
let message: Double
let country: String
let sunrise, sunset: Int
}
struct Weather: Codable {
let id: Int
let main, description, icon: String
}
struct Wind: Codable {
let speed: Double
let deg: Int
}
private func getWeatherData(url: String, parameters: [String : String]) {
let JsonURLString:[String: Any] = ["url": WEATHER_URL, "parameters": parameters]
print(JsonURLString)
let urlString = JsonURLString["url"] as? String
guard let url = URL(string: urlString!) else { return }
URLSession.shared.dataTask(with: url) { ( data, response, err ) in
DispatchQueue.main.sync {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let city = try decoder.decode(WeatherData.self, from: data)
self.weatherData.description = city.weather[0].description
self.weatherData.temperature = Int(city.main.temp - 273)
self.weatherData.city = city.name
self.weatherData.condition = city.weather[0].id
self.updateUIWeatherData()
} catch {
print(error)
self.cityLabel.text = "Connection issues"
}
}
}.resume()
}
The exact error I am getting is the following:
longitude = -0.1337, latitude = 51.50998
["parameters": ["lat": "51.50998", "long": "-0.1337", "appid": "xxxxxxxxxxxxxxxxxx"], "url": "https://api.openweathermap.org/data/2.5/weather"]
keyNotFound(CodingKeys(stringValue: "coord", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"coord\", intValue: nil) (\"coord\").", underlyingError: nil))
I have looked at the following example and don't see how this would apply. Any help would be appreciated.
Icon is not appearing. Here is my model:
import UIKit
class WeatherDataModel {
//Declare your model variables here
var temperature: Int = 0
var condition: Int = 0
var city: String = ""
var weatherIconName = ""
var description: String = ""
//This method turns a condition code into the name of the weather condition image
func updateWeatherIcon(condition: Int) -> String {
switch (condition) {
case 0...300 :
return "tstorm1"
case 301...500 :
return "light_rain"
case 501...600 :
return "shower3"
case 601...700 :
return "snow4"
case 701...771 :
return "fog"
case 772...799 :
return "tstorm3"
case 800 :
return "sunny"
case 801...804 :
return "cloudy2"
case 900...903, 905...1000 :
return "tstorm3"
case 903 :
return "snow5"
case 904 :
return "sunny"
default :
return "dunno"
}
}
}
I have added my own icons. I have added this in the do catch block.
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let city = try decoder.decode(WeatherData.self, from: data)
print(city)
self.weatherData.description = city.weather[0].description
self.weatherData.temperature = Int(city.main.temp - 273)
self.weatherData.city = city.name
self.weatherData.condition = city.weather[0].id
self.weatherData.weatherIconName = WeatherDataModel.updateWeatherIcon(self.weatherData.condition)
self.updateUIWeatherData()
} catch {
print(error)
self.cityLabel.text = "Connection issues"
}
The error I am getting this error now:
Instance member 'updateWeatherIcon' cannot be used on type 'WeatherDataModel'; did you mean to use a value of this type instead?
You are creating only the openweathermap URL but you ignore the parameters.
Use something like this for example URLComponents and URLQueryItem to build the URL query properly
private func getWeatherData(parameters: [String : String]) {
guard let lat = parameters["lat"],
let long = parameters["long"],
let appID = parameters["appid"] else { print("Invalid parameters"); return }
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")!
let queryItems = [URLQueryItem(name: "lat", value: lat),
URLQueryItem(name: "lon", value: long),
URLQueryItem(name: "appid", value: appID)]
urlComponents.queryItems = queryItems
guard let url = urlComponents.url else { return }
URLSession.shared.dataTask(with: url) { ( data, response, err ) in
DispatchQueue.main.async { // never, never, never sync !!
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let city = try decoder.decode(WeatherData.self, from: data)
print(city)
self.weatherData.description = city.weather[0].description
self.weatherData.temperature = Int(city.main.temp - 273)
self.weatherData.city = city.name
self.weatherData.condition = city.weather[0].id
self.updateUIWeatherData()
} catch {
print(error)
self.cityLabel.text = "Connection issues"
}
}
}.resume()
}
and pass only
["lat": "51.50998", "long": "-0.1337", "appid": "xxxxxxxxxxxxxxxxxx"]
as parameters.
Related
I am reading data from Firestore to be able to populate into expanding tableview cells. I have a really simple struct:
protocol PlanSerializable {
init?(dictionary:[String:Any])
}
struct Plan{
var menuItemName: String
var menuItemQuantity: Int
var menuItemPrice: Double
var dictionary: [String: Any] {
return [
"menuItemName": menuItemName,
"menuItemQuantity": menuItemQuantity,
"menuItemPrice": menuItemPrice
]
}
}
extension Plan : PlanSerializable {
init?(dictionary: [String : Any]) {
guard let menuItemName = dictionary["menuItemName"] as? String,
let menuItemQuantity = dictionary["menuItemQuantity"] as? Int,
let menuItemPrice = dictionary["menuItemPrice"] as? Double
else { return nil }
self.init(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity, menuItemPrice: menuItemPrice)
}
}
And this is embedded in this struct:
protocol ComplainSerializable {
init?(dictionary:[String:Any])
}
struct Complain{
var status: Bool
var header: String
var message: String
var timeStamp: Timestamp
var email: String
var planDetails: Plan
var dictionary: [String: Any] {
return [
"status": status,
"E-mail": header,
"Message": message,
"Time_Stamp": timeStamp,
"User_Email": email,
"planDetails": planDetails
]
}
}
extension Complain : ComplainSerializable {
init?(dictionary: [String : Any]) {
guard let status = dictionary["status"] as? Bool,
let header = dictionary["E-mail"] as? String,
let message = dictionary["Message"] as? String,
let timeStamp = dictionary["Time_Stamp"] as? Timestamp,
let email = dictionary["User_Email"] as? String,
let planDetails = dictionary["planDetails"] as? Plan
else { return nil }
self.init(status: status, header: header, message: message, timeStamp: timeStamp, email: email, planDetails: planDetails)
}
}
However, I am not able to query any data from Firestore which looks like this:
Here is my query, although I am just reading all the files:
let db = Firestore.firestore()
var messageArray = [Complain]()
func loadMenu() {
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
self.messageArray = documentSnapshot!.documents.compactMap({Complain(dictionary: $0.data())})
for plan in self.messageArray {
print("\(plan.email)")
}
DispatchQueue.main.async {
self.testTable.reloadData()
}
}
}
}
What am I doing wrong?
EDIT:
As suggested, here is the updated embedded struct:
import Foundation
// MARK: - Complain
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: PlanDetails
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
Using quicktype.io, you can generate the struct. From there, all you need to do is run this tiny fragment of code within your response handler.
var compainArray = [Complain]()
func loadMenu() {
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
guard let snapshot = documentSnapshot else {return}
for document in snapshot.documents {
if let jsonData = try? JSONSerialization.data(withJSONObject: document.data()){
if let converted = try? JSONDecoder().decode(Complain.self, from: jsonData){
self.compainArray.append(converted)
}
}
}
DispatchQueue.main.async {
self.testTable.reloadData()
}
}
}
}
Which will handle the looping, and mapping of certain variables. Let me know if you have any trouble with this.
I am parsing a json array of object into a model which works. In this object, there is an array to a value and I created another model to handle that array but when ever I pass this object, the internal array returns nil after casting as the model class. Any help is appreciated
JSON Sample
[
{
"code": "AF",
"code3": "AFG",
"dial_code": "+93",
"name": "Afghanistan",
"capital": "Kabul",
"region": "Asia",
"subregion": "Southern Asia",
"states": [
{
"code": "BDS",
"name": "Badakhshān",
"subdivision": null
},
{
"code": "BGL",
"name": "Baghlān",
"subdivision": null
}
]
}
}
]
MODEL
public struct LocaleInfo {
public var locale: Locale?
public var id: String? {
return locale?.identifier
}
public var country: String
public var code: String
// public var phoneCode: String
public var states: [LocalStateInfo]
public var flag: UIImage? {
return UIImage(named: "Countries.bundle/Images/\(code.uppercased())", in: Bundle.main, compatibleWith: nil)
}
public var currencyCode: String? {
return locale?.currencyCode
}
public var currencySymbol: String? {
return locale?.currencySymbol
}
public var currencyName: String? {
guard let currencyCode = currencyCode else { return nil }
return locale?.localizedString(forCurrencyCode: currencyCode)
}
init(country: String, code: String/*, phoneCode: String*/, states: [LocalStateInfo]) {
self.country = country
self.code = code
self.states = states
self.locale = Locale.availableIdentifiers.map { Locale(identifier: $0) }.first(where: { $0.regionCode == code })
}
}
public struct LocalStateInfo {
public var code: String
public var name: String
public var subdivision: String
}
Passing the JSON Body
func getInfo(completionHandler: #escaping (FetchResults) -> ()) {
let bundle = Bundle(for: LocalePickerViewController.self)
let path = "Countries.bundle/Data/CountryCodes"
guard let jsonPath = bundle.path(forResource: path, ofType: "json"),
let jsonData = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) else {
let error: (title: String?, message: String?) = (title: "ContryCodes Error", message: "No ContryCodes Bundle Access")
return completionHandler(FetchResults.error(error: error))
}
if let jsonObjects = (try? JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.allowFragments)) as? Array<Any> {
var result: [LocaleInfo] = []
for jsonObject in jsonObjects {
guard let countryObj = jsonObject as? Dictionary<String, Any> else { continue }
guard let country = countryObj["name"] as? String,
let code = countryObj["code"] as? String/*,
let phoneCode = countryObj["dial_code"] as? String*/ else {
fatalError("Broken here")
continue
}
log("countryObj state \(countryObj["states"] as? [LocalStateInfo])", .fuck)
log("countryObj \(countryObj)", .fuck)
let states = countryObj["states"] as? [LocalStateInfo] ?? [LocalStateInfo]()
let new = LocaleInfo(country: country, code: code/*, phoneCode: phoneCode*/, states: states)
result.append(new)
}
return completionHandler(FetchResults.success(response: result))
}
let error: (title: String?, message: String?) = (title: "JSON Error", message: "Couldn't parse json to Info")
return completionHandler(FetchResults.error(error: error))
}
let states = countryObj["states"] as? [LocalStateInfo] ?? [LocalStateInfo]()
is presumably the line that isn't working for you. But countryObj is just a dictionary straight from JSON:
guard let countryObj = jsonObject as? Dictionary<String, Any> else { continue }
Why would casting it to an array of LocalStateInfo work at at all? It's an array of dictionaries, and you need to parse each one out individually.
You've said using Codable would alter the "entire scope" of the library, I don't understand how this is the case. You can implement codable (or even just Decodable) without affecting any other file.
So i am fetching from fire base config data and trying to manipulate it as so.
This is my struct :
struct FireBaseConfiguration: Codable {
var updateTime: String = ""
var iOSMinVersionForceUpdate: String = ""
var iOSMinVersionMessageUpdate: String = ""
var iOSMinFirmwareVersion: String = ""
var privacyPolicyUrlFireBase: String = ""
var termOfUseUrlFireBase: String = ""
}
This is my parser method:
func fireBaseConfigVersionMapParser(dataString: String, version: String) -> FireBaseConfiguration? {
var fireBaseConfiguration: FireBaseConfiguration?
let data = dataString.data(using: .utf8)!
do {
if let jsonArray = try JSONSerialization.jsonObject(with: data, options : .allowFragments) as? NSDictionary {
let model = jsonArray.object(forKey: version)
let data = try JSONSerialization.data(withJSONObject: model!, options: .prettyPrinted)
do {
let parsedModel = try JSONDecoder().decode(FireBaseConfiguration.self, from: data)
print("parsedModel is: \(parsedModel)")
fireBaseConfiguration = parsedModel
} catch let error{
print(error)
}
} else {
print("bad json")
}
} catch let error{
print(error)
}
return fireBaseConfiguration
}
And this is the implementation in the vc:
self.remoteConfig?.fetch(withExpirationDuration: 0, completionHandler: { [unowned self] (status, error) in
if status == .success {
self.remoteConfig?.activateFetched()
guard let configVersionMap = self.remoteConfig?.configValue(forKey: "iOSConfigVersionMap").stringValue else { return }
if let configModel = self.parser.fireBaseConfigVersionMapParser(dataString: configVersionMap, version: self.getCurrentAppVersion()!) {
print(configModel)
print(configModel.iOSMinVersionForceUpdate)
print(configModel.iOSMinVersionMessageUpdate)
var doubleForceUpdate = Double(configModel.iOSMinVersionForceUpdate)
var doubleMessageUpdate = Double(configModel.iOSMinVersionMessageUpdate)
print(doubleForceUpdate)
print(doubleMessageUpdate)
}
} else {
print("Config not fetched")
print("Error: \(error?.localizedDescription ?? "No error available.")")
}
})
so the prints are so:
FireBaseConfiguration(updateTime: "13/7/2018", iOSMinVersionForceUpdate: "1.0.2", iOSMinVersionMessageUpdate: "1.0.2", iOSMinFirmwareVersion: "1.0.1", privacyPolicyUrlFireBase: "https://www.name.com/corporate/privacy-policy", termOfUseUrlFireBase: "https://www.name.com/corporate/terms-of-use")
1.0.2
1.0.2
nil
nil
any ideas?
It is a simple String, but it's not actually a valid Double (Double values do not have two decimal places). So this is why it is failing.
What I think you actually need is the ability to compare version numbers, there are a number of other answers on the site that can show you how to do this:
Compare two version strings in Swift
Compare version numbers in Objective-C
So you can just keep your version numbers as a String
I'm not sure how to init the array in Struct. I'm not able to fetch data from array, meanwhile I was manage to get the result from object (platform.status).
Am I init it wrongly ?
Any ideas ?
Here is Network Request :
func fetchServicePlatform(token: String, _key: String) {
let selectedUrl = URL(string: "\(mainUrl)/get_service")
let parameters: [String: String] = ["_key": _key]
var serviceList = [ServiceList]()
URLSession.shared.dataTask(with: setupURLRequest(selectedURL: selectedUrl!, parameters: parameters, token: token, email: "test#gmail.com")) { (data, response, error) in
if let err = error {
print("Failed to fetch API: \(err.localizedDescription)")
}
guard let data = data else { return }
do {
guard let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] else { return }
let platform = Platform(json: json)
if platform.status == "success" {
self.serviceList = platform.service_list
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
} catch let jsonErr {
print("Failed to fetch service platform: ", jsonErr.localizedDescription)
}
}.resume()
}
Here is JSON :
{
"status": "pass",
"service_list": [
{
"title": "Plumber",
"description": "Plumber",
"image": "https://s3-ap-southeast-1.heroku.com.png"
},
{
"title": "Cleaner",
"description": "Cleaner",
"image": "https://s3-ap-southeast-1.heroku.com.png"
}
]
}
Here is Struct :
struct Platform {
let service_list: [ServiceList]
let status: String
init(json: [String: Any]) {
service_list = [ServiceList(json: json["service_list"] as? [String: Any] ?? [:])]
status = json["status"] as? String ?? ""
}
}
struct ServiceList {
let title: String
let description: String
let image: String
init(json: [String: Any]) {
title = json["title"] as? String ?? ""
description = json["description"] as? String ?? ""
image = json["image"] as? String ?? ""
}
}
In your data json["service_list"] is an array of dictionaries,
You Can try out.
struct Platform {
var service_list: [ServiceList] = []
var status: String
init(json: [String: Any]) {
if let jsonArray = json["service_list"] as? [[String: Any]] {
for service in jsonArray {
service_list.append(ServiceList(json: service))
}
}
else{
service_list = []
}
status = json["status"] as? String ?? ""
}
}
init an array in the struct
struct MyData {
var dataArray:[Any] = []
var latitude: Float
var longitude: Float
}
You have to unwrap the dictionary as an array of dictionaries and then loop through it with a map or flatmap, where you use the value $0 as the value
guard let serviceList = (json["service_list"] as? [[String: Any]])?.flatmap({ServiceList(withDictionary: $0)})
Go with the below approach, this way it is simpler
Create your structure with Codable
struct Platform: Codable {
let service_list: [ServiceList]
let status: String
enum CodingKeys: String, CodingKey {
case service_list, status
}
}
struct ServiceList: Codable {
let title: String
let description: String
let image: String
enum CodingKeys: String, CodingKey {
case title, description, image
}
}
Your json data object
let json = """
{
"status": "pass",
"service_list": [
{
"title": "Plumber",
"description": "Plumber",
"image": "https://s3-ap-southeast-1.heroku.com.png"
},
{
"title": "Cleaner",
"description": "Cleaner",
"image": "https://s3-ap-southeast-1.heroku.com.png"
}
]
}
""".data(using: .utf8)!
Use the JSONDecoder to map the json object to structure
let decoder = JSONDecoder()
let platform = try! decoder.decode(Platform.self, from: json)
if platform.status == "pass"{
for service in platform.service_list{
print(service.title)
}
}
I've got a problem with decoding response data. Here is my request function
#IBAction func onGetCities(_ sender: UIButton) {
guard let url = URL(string: "http://somelink.com/city-list") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
print(JSON(data))
guard let data = data else { return }
do{
let cities = try JSONDecoder().decode([City].self, from: data)
print(cities)
}catch{
}
}
task.resume()
}
And City struct
struct City: Decodable {
let id: Int
let city: String
}
Here is the response data, I want to decode "items"
{
"offset": 0,
"limit": 10,
"items": [
{id: 0, name: "City name"},
{id: 1, name: "City name1"},
.....
]
}
You need to have nested structures that mirror your nested JSON:
struct City: Decodable {
let id: Int
let city: String
}
struct ResponseObject: Decodable {
let items: [City]
let offset: Int
let limit: Int
}
And then:
do {
let result = try JSONDecoder().decode(ResponseObject.self, from: data)
print(result)
let cities = result.items
print(cities)
} catch {
print(error)
}
Note, in your original example, you included a JSON key, updated_at which was the date represented in number of seconds since 1970 (standard UNIX representation). So, to decode that:
struct City: Decodable {
let id: Int
let city: String
}
struct ResponseObject: Decodable {
let items: [City]
let offset: Int
let limit: Int
let code: Int
let updatedAt: Date
enum CodingKeys: String, CodingKey {
case items, offset, limit, code
case updatedAt = "updated_at"
}
}
And then:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
do {
let result = try decoder.decode(ResponseObject.self, from: data)
print(result)
let cities = result.items
print(cities)
} catch {
print(error)
}