Parsing JSON corretto in Swift 3

Sto cercando di recuperare una risposta JSON e memorizzare i risultati in una variabile. Ho avuto versioni di questo codice nelle versioni precedenti di Swift, fino a quando non è stata rilasciata la versione GM di Xcode 8. Ho dato un’occhiata ad alcuni post simili su StackOverflow: Swift 2 Parsing JSON – Can not subscript un valore di tipo ‘AnyObject’ e JSON Parsing in Swift 3 .

Tuttavia, sembra che le idee trasmesse non si applichino in questo scenario.

Come analizzo correttamente la risposta JSON in Swift 3? Qualcosa è cambiato nel modo in cui JSON viene letto in Swift 3?

Di seguito è riportato il codice in questione (può essere eseguito in un parco giochi):

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)") } } } 

Modifica: Ecco un esempio dei risultati della chiamata API dopo la 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] 

Prima di tutto non caricare mai i dati in modo sincrono da un URL remoto , utilizzare sempre metodi asincroni come URLSession .

‘Qualsiasi’ non ha membri iscritti

si verifica perché il compilatore non ha idea di quale tipo siano gli oggetti intermedi (ad esempio currently in ["currently"]!["temperature"] ) e dal momento che si utilizzano tipi di raccolte di base come NSDictionary il compilatore non ha alcuna idea circa il genere.

Inoltre in Swift 3 è richiesto di informare il compilatore sul tipo di tutti gli oggetti con pedice.

È necessario eseguire il cast del risultato della serializzazione JSON sul tipo effettivo.

Questo codice utilizza URLSession e esclusivamente i tipi nativi 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() 

Per stampare tutte le coppie chiave / valore di currentConditions che potresti scrivere

  let currentConditions = parsedData["currently"] as! [String:Any] for (key, value) in currentConditions { print("\(key) - \(value) ") } 

Una nota riguardante jsonObject(with data :

Molti tutorial (sembra tutto) suggeriscono opzioni .mutableContainers o .mutableLeaves che non ha assolutamente senso in Swift. Le due opzioni sono opzioni legacy Objective-C per assegnare il risultato agli oggetti NSMutable... In Swift qualsiasi variabile è modificabile per impostazione predefinita e passare una di queste opzioni e assegnare il risultato a una costante non ha alcun effetto. Inoltre la maggior parte delle implementazioni non sta mai mutando il JSON deserializzato.

L’unica (rara) opzione che è utile in Swift è .allowFragments che è richiesta se l’object radice JSON può essere un tipo di valore ( String , Number , Bool o null ) piuttosto che uno dei tipi di raccolta ( array o dictionary ). Ma normalmente omettono il parametro options che significa Nessuna opzione .

================================================== =========================

Alcune considerazioni generali per analizzare JSON

JSON è un formato di testo ben organizzato. È molto facile leggere una stringa JSON. Leggi attentamente la stringa . Esistono solo sei diversi tipi: due tipi di raccolta e quattro tipi di valore.


I tipi di raccolta sono

  • Array – JSON: oggetti tra parentesi quadre [] – Swift: [Any] ma nella maggior parte dei casi [[String:Any]]
  • Dizionario – JSON: oggetti nelle parentesi graffe {} – Swift: [String:Any]

I tipi di valore sono

  • String – JSON: qualsiasi valore tra virgolette "Foo" , anche "123" o "false" – Swift: String
  • Numero – JSON: valori numerici non tra virgolette 123 o 123.0 – Swift: Int o Double
  • Bool – JSON: true o false non tra virgolette – Swift: true o false
  • null – JSON: null – Swift: NSNull

Secondo la specifica JSON tutte le chiavi dei dizionari devono essere String .


Fondamentalmente è sempre raccomandato l’uso di binding opzionali per scartare gli optionals in modo sicuro

Se l’object radice è un dizionario ( {} ), inoltrare il tipo a [String:Any]

 if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ... 

e recuperare i valori con le chiavi con ( OneOfSupportedJSONTypes è una raccolta JSON o un tipo di valore come descritto sopra).

 if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes { print(foo) } 

Se l’object radice è un array ( [] ), inserisci il tipo su [[String:Any]]

 if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ... 

e scorrere l’array con

 for item in parsedData { print(item) } 

Se hai bisogno di un articolo a un indice specifico, controlla anche se l’indice esiste

 if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2, let item = parsedData[2] as? OneOfSupportedJSONTypes { print(item) } } 

Nel raro caso in cui il JSON sia semplicemente uno dei tipi di valore – piuttosto che un tipo di raccolta – devi passare l’opzione .allowFragments e .allowFragments il risultato al tipo di valore appropriato, ad esempio

 if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ... 

Apple ha pubblicato un articolo completo sul blog Swift: Lavorare con JSON in Swift

Un grande cambiamento che è accaduto con Xcode 8 Beta 6 per Swift 3 era che ora importa come Any piuttosto che AnyObject .

Ciò significa che parsedData viene restituito come un dizionario molto probabilmente con il tipo [Any:Any] . Senza usare un debugger non potrei dirti esattamente cosa farà il tuo cast su NSDictionary ma l’errore che stai vedendo è perché dict!["currently"]! ha tipo Any

Quindi, come risolvi questo? Dal modo in cui l’hai fatto riferimento, presumo dict!["currently"]! è un dizionario e quindi hai molte opzioni:

Innanzitutto potresti fare qualcosa del genere:

 let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject] 

Questo ti darà un object dizionario che puoi quindi richiedere per i valori e così puoi ottenere la tua temperatura in questo modo:

 let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double 

O se preferisci puoi farlo in linea:

 let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double 

Spero che questo aiuti, temo di non aver avuto il tempo di scrivere un’app campione per testarlo.

Un’ultima nota: la cosa più semplice da fare, potrebbe essere semplicemente il cast del payload JSON in [String: AnyObject] all’inizio.

 let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary 
 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)") } 

Aggiornato successivamente la funzione isConnectToNetwork, grazie a questo post Controllare la connessione internet con Swift

Ho scritto un metodo extra per questo:

 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 } 

Così ora puoi chiamarlo facilmente nella tua app dove vuoi

 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 } } 

Ho costruito quicktype esattamente per questo scopo. Basta incollare il tuo campione JSON e quicktype genera questa gerarchia di tipi per i tuoi dati 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 } 

Genera anche codice di JSONSerialization.jsonObject per JSONSerialization.jsonObject valore di ritorno di JSONSerialization.jsonObject in una Forecast , incluso un costruttore di convenienza che accetta una stringa JSON in modo da poter analizzare rapidamente un valore di Forecast fortemente tipizzato e accedere ai relativi campi:

 let forecast = Forecast.from(json: jsonString)! print(forecast.daily.data[0].windGustTime) 

Puoi installare quicktype da npm con npm i -g quicktype o utilizzare l’interfaccia utente web per ottenere il codice generato completo da incollare nel tuo parco giochi.

Il problema è con il metodo di interazione API. L’analisi JSON viene modificata solo in Sintassi. Il problema principale riguarda il modo di recuperare i dati. Quello che stai usando è un modo sincrono di ottenere dati. Questo non funziona in ogni caso. Quello che dovresti usare è un modo asincrono per recuperare i dati. In questo modo, devi richiedere i dati tramite l’API e attendere che risponda con i dati. Puoi ottenerlo con una sessione di URL e librerie di terze parti come Alamofire. Di seguito è riportato il codice per il metodo di sessione 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()