J'essaie d'extraire une réponse JSON et de stocker les résultats dans une variable. Des versions de ce code ont fonctionné dans les versions précédentes de Swift, jusqu'à la publication de la version GM de Xcode 8. J'ai jeté un œil à quelques articles similaires sur StackOverflow: Swift 2 Analyse JSON - Impossible d'indiquer une valeur de type 'AnyObject' et Analyse JSON dans Swift 3 .
Cependant, il semble que les idées exprimées ici ne s’appliquent pas à ce scénario.
Comment analyser correctement la réponse JSON dans Swift 3? Est-ce que quelque chose a changé dans la façon dont JSON est lu dans Swift 3?
Vous trouverez ci-dessous le code en question (il peut être utilisé dans une cour de récréation):
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)")
}
}
}
Edit: Voici un exemple des résultats de l'appel de l'API après 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]
Tout d'abord, ne chargez jamais les données de manière synchrone à partir d'une URL distante, utilisez toujours des méthodes asynchrones telles que URLSession
.
'N'importe lequel' n'a pas de membre avec indice
se produit car le compilateur n'a aucune idée du type d'objet intermédiaire (par exemple, currently
dans ["currently"]!["temperature"]
) et, comme vous utilisez des types de collection Foundation tels que NSDictionary
, le compilateur n'a aucune idée du type.
De plus, dans Swift 3, il est nécessaire d’informer le compilateur du type d’objets tous indexés.
Vous devez convertir le résultat de la sérialisation JSON en type réel.
Ce code utilise URLSession
et exclusivement types natifs 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()
Pour imprimer toutes les paires clé/valeur de currentConditions
, vous pouvez écrire
let currentConditions = parsedData["currently"] as! [String:Any]
for (key, value) in currentConditions {
print("\(key) - \(value) ")
}
Une note concernant jsonObject(with data
:
De nombreux tutoriels (il semble que tous) suggèrent des options .mutableContainers
ou .mutableLeaves
, ce qui est complètement absurde dans Swift. Les deux options sont des options Objective-C héritées pour affecter le résultat à des objets NSMutable...
. Dans Swift, tout var
iable est modifiable par défaut et le fait de passer l'une de ces options et d'assigner le résultat à une constante let
n'a aucun effet. De plus, la plupart des implémentations ne modifient jamais le JSON désérialisé de toute façon.
La seule option (rare) utile dans Swift est .allowFragments
qui est requise si l'objet racine JSON peut être un type de valeur (String
, Number
, Bool
ou null
) plutôt qu'un des types de collection (array
ou dictionary
). Mais normalement, omettez le paramètre options
qui signifie Aucune option.
=============================================== =========================
JSON est un format de texte bien organisé. Il est très facile de lire une chaîne JSON. Lisez attentivement la chaîne. Il n'y a que six types différents - deux types de collection et quatre types de valeur.
Les types de collection sont
[]
- Swift: [Any]
mais dans la plupart des cas [[String:Any]]
{}
- Swift: [String:Any]
Les types de valeur sont
"Foo"
, même "123"
ou "false"
- Swift: String
123
ou 123.0
- Swift: Int
ou Double
true
ou false
pas entre guillemets doubles - Swift: true
ou false
null
- Swift: NSNull
Selon la spécification JSON, toutes les clés des dictionnaires doivent être String
.
Il est toujours recommandé d'utiliser des liaisons optionnelles pour déballer les options en toute sécurité
Si l'objet racine est un dictionnaire ({}
), convertissez le type en [String:Any]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...
et récupérez les valeurs à l'aide de clés avec (OneOfSupportedJSONTypes
est soit une collection JSON, soit le type de valeur, comme décrit ci-dessus.)
if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
print(foo)
}
Si l'objet racine est un tableau ([]
), convertissez le type en [[String:Any]]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...
et parcourir le tableau avec
for item in parsedData {
print(item)
}
Si vous avez besoin d'un élément à un index spécifique, vérifiez également si l'index existe
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
let item = parsedData[2] as? OneOfSupportedJSONTypes {
print(item)
}
}
Dans les rares cas où le JSON est simplement l'un des types de valeur - plutôt qu'un type de collection - vous devez passer l'option .allowFragments
et convertir le résultat en type de valeur approprié, par exemple
if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...
Apple a publié un article complet dans le Swift Blog: Utilisation de JSON dans Swift
Update: dans Swift 4+, le protocole Codable
fournit un moyen plus pratique d'analyser JSON directement dans des structures/classes.
Un gros changement survenu avec Xcode 8 Beta 6 pour Swift 3 est que l'identifiant est maintenant importé sous la forme Any
plutôt que AnyObject
.
Cela signifie que parsedData
est renvoyé sous forme de dictionnaire de type le plus probable avec le type [Any:Any]
. Sans utiliser un débogueur, je ne saurais vous dire exactement ce que votre conversion en NSDictionary
fera, mais l'erreur que vous voyez est due au fait que dict!["currently"]!
a le type Any
Alors, comment résolvez-vous cela? De la manière dont vous l'avez référencé, je suppose que dict!["currently"]!
est un dictionnaire et vous avez donc beaucoup d'options:
D'abord, vous pourriez faire quelque chose comme ça:
let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]
Cela vous donnera un objet dictionnaire que vous pourrez ensuite interroger pour obtenir des valeurs et vous pourrez ainsi obtenir votre température comme ceci:
let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double
Ou si vous préférez, vous pouvez le faire en ligne:
let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double
J'espère que cela aide, je crains de ne pas avoir eu le temps d'écrire un exemple d'application pour le tester.
Une dernière remarque: la solution la plus simple consiste peut-être simplement à convertir la charge JSON en [String: AnyObject]
dès le début.
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)")
}
J'ai construit quicktype exactement à cette fin. Il suffit de coller votre échantillon JSON et quicktype génère cette hiérarchie de types pour vos données d'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
}
Il génère également un code de marshaling sans dépendance pour insérer la valeur de retour JSONSerialization.jsonObject
dans une variable Forecast
, y compris un constructeur de commodité prenant une chaîne JSON afin que vous puissiez analyser rapidement une valeur Forecast
fortement typée et accéder à ses champs:
let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)
Vous pouvez installer quicktype à partir de npm avec npm i -g quicktype
ou utilisez l'interface Web pour obtenir le code complet généré à coller dans votre terrain de jeu.
Mise à jour de la fonction isConnectToNetwork par la suite, grâce à cet article Vérifiez la connexion Internet avec Swift
j'ai écrit une méthode supplémentaire pour cela:
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
}
Alors maintenant, vous pouvez facilement appeler cela dans votre application où vous voulez
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
}
}
Le problème concerne la méthode d'interaction de l'API. L'analyse JSON est modifiée uniquement dans la syntaxe. Le problème principal concerne le mode de récupération des données. Ce que vous utilisez est un moyen synchrone d’obtenir des données. Cela ne fonctionne pas dans tous les cas. Ce que vous devriez utiliser est une méthode asynchrone pour extraire des données. De cette manière, vous devez demander des données via l'API et attendre qu'elle réponde avec les données. Vous pouvez y parvenir avec une session URL et des bibliothèques tierces comme Alamofire. Vous trouverez ci-dessous la méthode Code for URL Session.
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()
Swift a une inférence de type puissante. Permet de se débarrasser de "si laissez" ou "garde laisse" passe-passe-passe et force le déroulement en utilisant une approche fonctionnelle:
let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
/// 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
}
let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound
Une seule ligne de code et pas de décompression forcée ni de transtypage manuel. Ce code fonctionne dans la cour de récréation, vous pouvez donc le copier et le vérifier. Voici une implémentation sur GitHub.