Analizando correctamente JSON en Swift 3
Estoy intentando obtener una respuesta JSON y almacenar los resultados en una variable. He tenido versiones de este código funcionando en versiones anteriores de Swift, hasta que se lanzó la versión GM de Xcode 8. Eché un vistazo a algunas publicaciones similares en StackOverflow: Swift 2 Parsing JSON: no se puede subscribir un valor de tipo 'AnyObject' y JSON Parsing en Swift 3 .
Sin embargo, parece que las ideas allí transmitidas no se aplican en este escenario.
¿Cómo analizo correctamente la respuesta JSON en Swift 3? ¿Ha cambiado algo en la forma en que se lee JSON en Swift 3?
A continuación se muestra el código en cuestión (se puede ejecutar en un patio de juegos):
import Cocoa
let url = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
if let url = NSURL(string: url) {
if let data = try? Data(contentsOf: url as URL) {
do {
let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments)
//Store response in NSDictionary for easy access
let dict = parsedData as? NSDictionary
let currentConditions = "\(dict!["currently"]!)"
//This produces an error, Type 'Any' has no subscript members
let currentTemperatureF = ("\(dict!["currently"]!["temperature"]!!)" as NSString).doubleValue
//Display all current conditions from API
print(currentConditions)
//Output the current temperature in Fahrenheit
print(currentTemperatureF)
}
//else throw an error detailing what went wrong
catch let error as NSError {
print("Details of JSON parsing error:\n \(error)")
}
}
}
Editar: aquí hay una muestra de los resultados de la llamada API despuésprint(currentConditions)
["icon": partly-cloudy-night, "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precipIntensity": 0, "windSpeed": 6.04, "summary": Partly Cloudy, "ozone": 321.13, "temperature": 49.45, "dewPoint": 41.75, "apparentTemperature": 47, "windBearing": 332, "cloudCover": 0.28, "time": 1480846460]
En primer lugar, nunca cargue datos de forma sincrónica desde una URL remota , utilice siempre métodos asincrónicos como URLSession
.
'Cualquiera' no tiene miembros con subíndice
ocurre porque el compilador no tiene idea de qué tipo son los objetos intermedios (por ejemplo, currently
en ["currently"]!["temperature"]
) y dado que está utilizando tipos de colección Foundation, NSDictionary
el compilador no tiene ninguna idea sobre el tipo.
Además, en Swift 3 es necesario informar al compilador sobre el tipo de todos los objetos con subíndice.
Debe convertir el resultado de la serialización JSON al tipo real.
Este código utiliza URLSession
exclusivamente tipos nativos Swift .
let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print(error)
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
let currentConditions = parsedData["currently"] as! [String:Any]
print(currentConditions)
let currentTemperatureF = currentConditions["temperature"] as! Double
print(currentTemperatureF)
} catch let error as NSError {
print(error)
}
}
}.resume()
Para imprimir todos los pares clave/valor, currentConditions
puede escribir
let currentConditions = parsedData["currently"] as! [String:Any]
for (key, value) in currentConditions {
print("\(key) - \(value) ")
}
Una nota sobre jsonObject(with data
:
Muchos (al parecer todos) tutoriales sugieren .mutableContainers
opciones .mutableLeaves
que son completamente una tontería en Swift. Las dos opciones son opciones heredadas de Objective-C para asignar el resultado a NSMutable...
objetos. En Swift, cualquier var
iable es mutable de forma predeterminada y pasar cualquiera de esas opciones y asignar el resultado a una let
constante no tiene ningún efecto. Además, la mayoría de las implementaciones nunca mutan el JSON deserializado de todos modos.
La única opción (rara) que es útil en Swift es .allowFragments
la que se requiere si el objeto raíz JSON pudiera ser un tipo de valor ( String
, Number
o Bool
) null
en lugar de uno de los tipos de colección ( array
o dictionary
). Pero normalmente omite el options
parámetro que significa Sin opciones .
==================================================== ===========================
Algunas consideraciones generales para analizar JSON
JSON es un formato de texto bien organizado. Es muy fácil leer una cadena JSON. Lea atentamente la cadena . Sólo hay seis tipos diferentes: dos tipos de colección y cuatro tipos de valor.
Los tipos de colección son
- Array - JSON: objetos entre corchetes
[]
- Swift:[Any]
pero en la mayoría de los casos[[String:Any]]
- Diccionario - JSON: objetos entre llaves
{}
- Swift:[String:Any]
Los tipos de valores son
- Cadena - JSON: cualquier valor entre comillas dobles
"Foo"
, pares"123"
o"false"
– Swift:String
- Número - JSON: valores numéricos sin comillas dobles
123
o123.0
– Swift:Int
oDouble
- Bool - JSON:
true
ofalse
no entre comillas dobles - Swift:true
ofalse
- nulo - JSON:
null
– Rápido:NSNull
Según la especificación JSON, todas las claves de los diccionarios deben ser String
.
Básicamente, siempre se recomienda utilizar enlaces opcionales para desenvolver los opcionales de forma segura.
Si el objeto raíz es un diccionario ( {}
), convierta el tipo a[String:Any]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...
y recuperar valores por claves con ( OneOfSupportedJSONTypes
es una colección JSON o un tipo de valor como se describe anteriormente).
if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
print(foo)
}
Si el objeto raíz es una matriz ( []
), convierta el tipo a[[String:Any]]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...
e iterar a través de la matriz con
for item in parsedData {
print(item)
}
Si necesita un artículo en un índice específico, verifique también si el índice existe.
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
let item = parsedData[2] as? OneOfSupportedJSONTypes {
print(item)
}
}
En el raro caso de que JSON sea simplemente uno de los tipos de valor, en lugar de un tipo de colección, debe pasar la .allowFragments
opción y convertir el resultado al tipo de valor apropiado, por ejemplo.
if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...
Apple ha publicado un artículo completo en el Blog de Swift: Trabajar con JSON en Swift
==================================================== ===========================
En Swift 4+, el Codable
protocolo proporciona una forma más conveniente de analizar JSON directamente en estructuras/clases.
Por ejemplo, el ejemplo JSON proporcionado en la pregunta (ligeramente modificado)
let jsonString = """
{"icon": "partly-cloudy-night", "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precip_intensity": 0, "wind_speed": 6.04, "summary": "Partly Cloudy", "ozone": 321.13, "temperature": 49.45, "dew_point": 41.75, "apparent_temperature": 47, "wind_bearing": 332, "cloud_cover": 0.28, "time": 1480846460}
"""
se puede decodificar en la estructura Weather
. Los tipos de Swift son los mismos que los descritos anteriormente. Hay algunas opciones adicionales:
- Las cadenas que representan an
URL
se pueden decodificar directamente comoURL
. - El
time
número entero se puede decodificar comoDate
con eldateDecodingStrategy
.secondsSince1970
. - Las claves JSON de Snaked_cased se pueden convertir a camelCase con el
keyDecodingStrategy
.convertFromSnakeCase
struct Weather: Decodable {
let icon, summary: String
let pressure: Double, humidity, windSpeed : Double
let ozone, temperature, dewPoint, cloudCover: Double
let precipProbability, precipIntensity, apparentTemperature, windBearing : Int
let time: Date
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Weather.self, from: data)
print(result)
} catch {
print(error)
}
Otras fuentes codificables:
- Apple: codificación y decodificación de tipos personalizados
- HackingWithSwift: hoja de trucos codificable
- Ray Wenderlich: codificación y decodificación en Swift
Un gran cambio que ocurrió con Xcode 8 Beta 6 para Swift 3 fue que id ahora se importa como Any
en lugar de AnyObject
.
Esto significa que parsedData
se devuelve como un diccionario probablemente con el tipo [Any:Any]
. Sin usar un depurador, no podría decirle exactamente qué NSDictionary
hará su conversión, pero el error que está viendo se debe a que dict!["currently"]!
tiene tipoAny
Entonces, ¿cómo se soluciona esto? Por la forma en que lo ha referenciado, supongo dict!["currently"]!
que es un diccionario y, por lo tanto, tiene muchas opciones:
Primero podrías hacer algo como esto:
let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]
Esto le dará un objeto de diccionario que luego podrá consultar para obtener valores y así obtener su temperatura de esta manera:
let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double
O si lo prefieres puedes hacerlo en línea:
let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double
Espero que esto ayude, me temo que no he tenido tiempo de escribir una aplicación de muestra para probarla.
Una nota final: lo más fácil de hacer podría ser simplemente lanzar la carga útil JSON [String: AnyObject]
desde el principio.
let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"
let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
if let names = json["names"] as? [String]
{
print(names)
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
Construí QuickType exactamente para este propósito. Simplemente pegue su JSON de muestra y QuickType generará esta jerarquía de tipos para sus datos API:
struct Forecast {
let hourly: Hourly
let daily: Daily
let currently: Currently
let flags: Flags
let longitude: Double
let latitude: Double
let offset: Int
let timezone: String
}
struct Hourly {
let icon: String
let data: [Currently]
let summary: String
}
struct Daily {
let icon: String
let data: [Datum]
let summary: String
}
struct Datum {
let precipIntensityMax: Double
let apparentTemperatureMinTime: Int
let apparentTemperatureLowTime: Int
let apparentTemperatureHighTime: Int
let apparentTemperatureHigh: Double
let apparentTemperatureLow: Double
let apparentTemperatureMaxTime: Int
let apparentTemperatureMax: Double
let apparentTemperatureMin: Double
let icon: String
let dewPoint: Double
let cloudCover: Double
let humidity: Double
let ozone: Double
let moonPhase: Double
let precipIntensity: Double
let temperatureHigh: Double
let pressure: Double
let precipProbability: Double
let precipIntensityMaxTime: Int
let precipType: String?
let sunriseTime: Int
let summary: String
let sunsetTime: Int
let temperatureMax: Double
let time: Int
let temperatureLow: Double
let temperatureHighTime: Int
let temperatureLowTime: Int
let temperatureMin: Double
let temperatureMaxTime: Int
let temperatureMinTime: Int
let uvIndexTime: Int
let windGust: Double
let uvIndex: Int
let windBearing: Int
let windGustTime: Int
let windSpeed: Double
}
struct Currently {
let precipProbability: Double
let humidity: Double
let cloudCover: Double
let apparentTemperature: Double
let dewPoint: Double
let ozone: Double
let icon: String
let precipIntensity: Double
let temperature: Double
let pressure: Double
let precipType: String?
let summary: String
let uvIndex: Int
let windGust: Double
let time: Int
let windBearing: Int
let windSpeed: Double
}
struct Flags {
let sources: [String]
let isdStations: [String]
let units: String
}
También genera código de clasificación libre de dependencias para convertir el valor de retorno de JSONSerialization.jsonObject
en a Forecast
, incluido un práctico constructor que toma una cadena JSON para que pueda analizar rápidamente un Forecast
valor fuertemente tipado y acceder a sus campos:
let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)
Puede instalar Quicktype desde npm npm i -g quicktype
o usar la interfaz de usuario web para obtener el código generado completo y pegarlo en su área de juegos.