web-dev-qa-db-fra.com

Analyser correctement JSON dans Swift 3

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]
109
user2563039

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 variable 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.

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

Quelques considérations générales pour analyser JSON

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

  • Tableau - JSON: objets entre crochets [] - Swift: [Any] mais dans la plupart des cas [[String:Any]]
  • Dictionnaire - JSON: objets entre accolades {} - Swift: [String:Any]

Les types de valeur sont

  • String - JSON: toute valeur entre guillemets "Foo", même "123" ou "false" - Swift: String
  • Number - JSON: valeurs numériques pas entre guillemets 123 ou 123.0 - Swift: Int ou Double
  • Bool - JSON: true ou falsepas entre guillemets doubles - Swift: true ou false
  • null - JSON: 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.

157
vadian

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>
12
discorevilo
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)")
}
6
BhuShan PaWar

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.

4
David Siegel

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
                }
  }
3
Marco Weber

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()
0
Arun K

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:

  1. Voici notre JSON. Nous pouvons utiliser JSON en option ou habituel. J'utilise facultatif dans notre exemple:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]

  1. Fonctions d'assistance. Nous devons les écrire une seule fois, puis les réutiliser avec n’importe quel dictionnaire:

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

  1. Et voici notre magie - extraire la valeur:

    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.

0
J. Doe