parser - Analizando correctamente JSON en Swift 3
json swift (7)
Construí quicktype exactamente para este propósito. Simplemente pegue su JSON de muestra y quicktype genera esta jerarquía de tipos para sus datos de 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
JSONSerialization.jsonObject
sin dependencia para obtener el valor de retorno de
JSONSerialization.jsonObject
en un
Forecast
, incluido un constructor de conveniencia que toma una cadena JSON para que pueda analizar rápidamente un valor de
Forecast
fuertemente tipado y acceder a sus campos:
let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)
Puede instalar quicktype desde npm con
npm i -g quicktype
o
usar la interfaz
de
usuario web
para obtener el código completo generado para pegarlo en su área de juegos.
Estoy tratando de obtener una respuesta JSON y almacenar los resultados en una variable. He tenido versiones de este código que funcionan 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 subíndice un valor de tipo ''AnyObject'' y JSON Parsing en Swift 3 .
Sin embargo, parece que las ideas transmitidas allí 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 parque infantil):
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és de
print(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]
El problema está en el método de interacción de la API. El análisis JSON solo se cambia en la sintaxis. El principal problema es con la forma de obtener datos. Lo que está utilizando es una forma síncrona de obtener datos. Esto no funciona en todos los casos. Lo que deberías usar es una forma asincrónica de recuperar datos. De esta manera, debe solicitar datos a través de la API y esperar a que responda con datos. Puede lograr esto con una sesión de URL y bibliotecas de terceros como Alamofire. A continuación se muestra el código para el método de sesión URL.
let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
let url = URL.init(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
guard error == nil else {
print(error)
}
do {
let Data = try JSONSerialization.jsonObject(with: data!) as! [String:Any] // Note if your data is coming in Array you should be using [Any]()
//Now your data is parsed in Data variable and you can use it normally
let currentConditions = Data["currently"] as! [String:Any]
print(currentConditions)
let currentTemperatureF = currentConditions["temperature"] as! Double
print(currentTemperatureF)
} catch let error as NSError {
print(error)
}
}.resume()
En primer lugar,
nunca cargue datos sincrónicamente desde una URL remota
, use métodos siempre asíncronos como
URLSession
.
''Any'' no tiene miembros de subíndice
se produce 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 de Foundation como
NSDictionary
el compilador no tiene ninguna idea sobre el tipo.
Además, en Swift 3 se requiere informar al compilador sobre el tipo de todos los objetos suscritos.
Debe convertir el resultado de la serialización JSON al tipo real.
Este código usa
URLSession
y tipos nativos
exclusivamente
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 de
currentConditions
, podría escribir
let currentConditions = parsedData["currently"] as! [String:Any]
for (key, value) in currentConditions {
print("/(key) - /(value) ")
}
Una nota sobre
jsonObject(with data
:
Muchos (parece que todos) los tutoriales sugieren opciones
.mutableContainers
o
.mutableLeaves
, lo cual es completamente absurdo en Swift.
Las dos opciones son opciones heredadas de Objective-C para asignar el resultado a
NSMutable...
objetos.
En Swift, cualquier variable es mutable por defecto y pasar cualquiera de esas opciones y asignar el resultado a una constante
let
no tiene ningún efecto.
Además, la mayoría de las implementaciones nunca están mutando el JSON deserializado de todos modos.
La única opción (rara) que es útil en Swift es
.allowFragments
que es necesaria si el objeto raíz JSON podría ser un tipo de valor (
String
,
Number
,
Bool
o
null
) en lugar de uno de los tipos de colección (
array
o
dictionary
).
Pero normalmente omite el parámetro de
options
, 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. Lee la cuerda cuidadosamente . Solo hay seis tipos diferentes: dos tipos de colección y cuatro tipos de valor.
Los tipos de colección son
-
Matriz
- 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 valor son
-
Cadena
- JSON: cualquier valor entre comillas dobles
"Foo"
, incluso"123"
o"false"
- Swift:String
-
Número
- JSON: valores numéricos que
no están
entre comillas dobles
123
o123.0
- Swift:Int
oDouble
-
Bool
- JSON:
true
ofalse
no entre comillas dobles - Swift:true
ofalse
-
nulo
- JSON:
null
- Swift:NSNull
De acuerdo con la especificación JSON, todas las claves en 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 (
{}
), envíe el tipo a
[String:Any]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...
y recuperar valores mediante 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 (
[]
), envíe 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 elemento 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 opción
.allowFragments
y emitir 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: Trabajando con JSON en Swift
================================================== =========================
En Swift 4+, el protocolo
Codable
proporciona una forma más conveniente de analizar JSON directamente en estructuras / clases.
Por ejemplo, la muestra JSON dada en la pregunta (ligeramente modificada)
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 Swift son los mismos que los descritos anteriormente.
Hay algunas opciones adicionales:
-
Las cadenas que representan una
URL
pueden decodificarse directamente comoURL
. -
El entero de
time
se puede decodificar comoDate
condateDecodingStrategy
.secondsSince1970
. -
Las
claves JSON
snaked_cased
se pueden convertir a
camelCase
con
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:
Swift tiene una poderosa inferencia de tipos. Vamos a deshacernos de la placa repetitiva "if let" o "guard let" y forzar el desenvolvimiento utilizando un enfoque funcional:
- Aquí está nuestro JSON. Podemos usar JSON opcional o habitual. Estoy usando opcional en nuestro ejemplo:
let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
- Funciones de ayuda. Necesitamos escribirlos solo una vez y luego reutilizarlos con cualquier diccionario:
/// Curry
public func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
return { a in
{ f(a, $0) }
}
}
/// Function that takes key and optional dictionary and returns optional value
public func extract<Key, Value>(_ key: Key, _ json: Dictionary<Key, Any>?) -> Value? {
return json.flatMap {
cast($0[key])
}
}
/// Function that takes key and return function that takes optional dictionary and returns optional value
public func extract<Key, Value>(_ key: Key) -> (Dictionary<Key, Any>?) -> Value? {
return curry(extract)(key)
}
/// Precedence group for our operator
precedencegroup RightApplyPrecedence {
associativity: right
higherThan: AssignmentPrecedence
lowerThan: TernaryPrecedence
}
/// Apply. g § f § a === g(f(a))
infix operator § : RightApplyPrecedence
public func §<A, B>(_ f: (A) -> B, _ a: A) -> B {
return f(a)
}
/// Wrapper around operator "as".
public func cast<A, B>(_ a: A) -> B? {
return a as? B
}
- Y aquí está nuestra magia: extraer el valor:
let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound
Solo una línea de código y sin desenredos forzados o fundición manual. Este código funciona en el patio de recreo, por lo que puede copiarlo y verificarlo. Aquí hay una implementación en GitHub.
Un gran cambio que sucedió con Xcode 8 Beta 6 para Swift 3 fue que la identificación ahora se importa como
Any
lugar de
AnyObject
.
Esto significa que
parsedData
se devuelve como un diccionario de lo más probable con el tipo
[Any:Any]
.
Sin usar un depurador, no podría decirte exactamente qué hará tu lanzamiento a
NSDictionary
, ¡pero el error que estás viendo es porque
dict!["currently"]!
tiene tipo
Any
Entonces, ¿cómo resuelves esto?
Por la forma en que lo ha referenciado, supongo que
dict!["currently"]!
es un diccionario y por eso tienes 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 puede consultar para valores y así puede obtener su temperatura de esta manera:
let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double
O si lo prefiere, puede 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 JSON en
[String: AnyObject]
desde el principio.
let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
Actualizó la función isConnectToNetwork después, gracias a esta publicación Compruebe la conexión a Internet con Swift
Escribí un método adicional para ello:
import SystemConfiguration
func loadingJSON(_ link:String, postString:String, completionHandler: @escaping (_ JSONObject: AnyObject) -> ()) {
if(isConnectedToNetwork() == false){
completionHandler("-1" as AnyObject)
return
}
let request = NSMutableURLRequest(url: URL(string: link)!)
request.httpMethod = "POST"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
guard error == nil && data != nil else { // check for fundamental networking error
print("error=/(error)")
return
}
if let httpStatus = response as? HTTPURLResponse , httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is /(httpStatus.statusCode)")
print("response = /(response)")
}
//JSON successfull
do {
let parseJSON = try JSONSerialization.jsonObject(with: data!, options: .allowFragments)
DispatchQueue.main.async(execute: {
completionHandler(parseJSON as AnyObject)
});
} catch let error as NSError {
print("Failed to load: /(error.localizedDescription)")
}
}
task.resume()
}
func isConnectedToNetwork() -> Bool {
var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
zeroAddress.sin_family = sa_family_t(AF_INET)
let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
}
}
var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
return false
}
let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
let ret = (isReachable && !needsConnection)
return ret
}
Así que ahora puedes llamar fácilmente a esto en tu aplicación donde quieras
loadingJSON("yourDomain.com/login.php", postString:"email=/(userEmail!)&password=/(password!)") {
parseJSON in
if(String(describing: parseJSON) == "-1"){
print("No Internet")
} else {
if let loginSuccessfull = parseJSON["loginSuccessfull"] as? Bool {
//... do stuff
}
}
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)")
}