Analisando corretamente o JSON no Swift 3

123

Estou tentando buscar uma resposta JSON e armazenar os resultados em uma variável. Eu tive versões desse código funcionando em versões anteriores do Swift, até que a versão GM do Xcode 8 foi lançada. Dei uma olhada em algumas postagens semelhantes no StackOverflow: Swift 2 Parsing JSON - Não é possível subscrever um valor do tipo 'AnyObject' e JSON Parsing no Swift 3 .

No entanto, parece que as idéias transmitidas não se aplicam nesse cenário.

Como analiso corretamente a resposta JSON no Swift 3? Alguma coisa mudou na maneira como o JSON é lido no Swift 3?

Abaixo está o código em questão (ele pode ser executado em um playground):

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: Aqui está uma amostra dos resultados da chamada da API apó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]
user2563039
fonte
Você pode colocar os dados de amostra retornados da sua chamada à API?
Utilizador
1
Sim, acabei de adicionar uma amostra dos resultados impressos após a impressão (currentConditions). Espero que ajude.
user2563039
Json análise em swift4 usando codificável protocolo stackoverflow.com/a/52931265/9316566
Naser Mohamed

Respostas:

172

Antes de tudo, nunca carregue dados de forma síncrona de uma URL remota , use sempre métodos assíncronos como URLSession.

'Any' não possui membros subscritos

ocorre porque o compilador não tem idéia de que tipo são os objetos intermediários (por exemplo, currentlyem ["currently"]!["temperature"]) e como você está usando tipos de coleção do Foundation, como NSDictionaryo compilador não tem idéia do tipo.

Além disso, no Swift 3, é necessário informar o compilador sobre o tipo de todos os objetos subscritos.

Você deve converter o resultado da serialização JSON no tipo real.

Este código usa URLSessione 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 os pares de chave / valor, currentConditionsvocê pode escrever

 let currentConditions = parsedData["currently"] as! [String:Any]

  for (key, value) in currentConditions {
    print("\(key) - \(value) ")
  }

Uma observação sobre jsonObject(with data:

Muitos (parece tudo) tutoriais sugerem .mutableContainersou .mutableLeavesopções que são completamente sem sentido no Swift. As duas opções são opções legadas de Objective-C para atribuir o resultado aos NSMutable...objetos. No Swift, qualquer varresponsável é mutável por padrão e passar qualquer uma dessas opções e atribuir o resultado a uma letconstante não tem efeito algum. Além disso, a maioria das implementações nunca está alterando o JSON desserializado de qualquer maneira.

A única opção (raro) que é útil no Swift é .allowFragmentsque é necessário se se o objecto raiz JSON poderia ser um tipo de valor ( String, Number, Boolou null) em vez de um dos tipos de recolha ( arrayou dictionary). Mas normalmente omita o optionsparâmetro que significa Sem opções .

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

Algumas considerações gerais para analisar JSON

JSON é um formato de texto bem organizado. É muito fácil ler uma string JSON. Leia a string com atenção . Existem apenas seis tipos diferentes - dois tipos de coleção e quatro tipos de valor.


Os tipos de coleção são

  • Matriz - JSON: objetos entre colchetes []- Swift: [Any]mas na maioria dos casos[[String:Any]]
  • Dicionário - JSON: objetos em chaves {}- Swift:[String:Any]

Os tipos de valor são

  • String - JSON: qualquer valor entre aspas duplas "Foo", par "123"ou "false"- Swift:String
  • Número - JSON: valores numéricos que não estão entre aspas duplas 123ou 123.0- Swift: IntouDouble
  • Bool - JSON: trueou false não entre aspas duplas - Swift: trueoufalse
  • null - JSON: null- Rápido:NSNull

De acordo com a especificação JSON, todas as chaves nos dicionários devem ser String.


Basicamente, é sempre recomendável usar ligações opcionais para desembrulhar opcionais com segurança

Se o objeto raiz for um dicionário ( {}), converta o tipo para[String:Any]

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

e recupere valores por chaves com ( OneOfSupportedJSONTypesé a coleção JSON ou o tipo de valor, conforme descrito acima.)

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

Se o objeto raiz for um array ( []), converta o tipo para[[String:Any]]

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

e percorra a matriz com

for item in parsedData {
    print(item)
}

Se você precisar de um item em um índice específico, verifique também se o índice existe

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

Nos raros casos em que o JSON é simplesmente um dos tipos de valor - em vez de um tipo de coleção - você deve passar a .allowFragmentsopção e converter o resultado no tipo de valor apropriado, por exemplo

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

A Apple publicou um artigo abrangente no Blog Swift: Trabalhando com JSON no Swift


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

No Swift 4+, o Codableprotocolo fornece uma maneira mais conveniente de analisar o JSON diretamente em estruturas / classes.

Por exemplo, a amostra JSON fornecida na pergunta (ligeiramente 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}
"""

pode ser decodificado na estrutura Weather. Os tipos Swift são os mesmos descritos acima. Existem algumas opções adicionais:

  • Strings representando um URLpodem ser decodificadas diretamente como URL.
  • O timenúmero inteiro pode ser decodificado como Datecom o dateDecodingStrategy .secondsSince1970.
  • As chaves JSON snaked_cased podem ser convertidas em camelCase com okeyDecodingStrategy .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)
}

Outras fontes codificáveis:

vadian
fonte
Isso é muito útil. Só estou curioso para saber por que o código acima não mostra saída visível quando é executado em um playground.
user2563039
Como mencionado acima, você precisa converter o resultado ambíguo de dict!["currently"]!um dicionário no qual o compilador possa inferir com segurança a assinatura de chave subsequente.
vadian 10/09/16
1
O artigo da Apple é bem legal, mas não consegui rodar com o swift 3 por algum motivo. Ele reclama do Type [String: Any]? não possui membros subscritos. Também havia outros problemas, mas consegui contornar isso. Alguém tem um exemplo de seu código que realmente é executado?
Shades
@Shades Faça uma pergunta e publique seu código. Provavelmente, seu problema está relacionado a opcionais desembrulhados.
Vadian
Você pode colocar os dados de amostra retornados da sua chamada à API?
Utilizador
12

Uma grande mudança que aconteceu com o Xcode 8 Beta 6 para o Swift 3 foi que o id agora importa mais Anydo que AnyObject.

Isso significa que parsedDataé retornado como um dicionário do mais provável com o tipo [Any:Any]. Sem usar um depurador, eu não poderia dizer exatamente o que seu elenco NSDictionaryfará, mas o erro que você está vendo é porque dict!["currently"]!tem o tipoAny

Então, como você resolve isso? Pela maneira como você o referenciou, presumo que dict!["currently"]!seja um dicionário e, portanto, você tem muitas opções:

Primeiro, você poderia fazer algo assim:

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

Isso fornecerá um objeto de dicionário que você poderá consultar por valores e obter sua temperatura assim:

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

Ou, se você preferir, pode fazê-lo na linha:

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

Espero que isso ajude, receio que não tenha tido tempo de escrever um aplicativo de amostra para testá-lo.

Uma observação final: a coisa mais fácil a se fazer, pode ser simplesmente converter a carga JSON [String: AnyObject]no início.

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
discorevilo
fonte
dict!["currently"]! as! [String: String]irá travar
vadian
O ponto em que caiu foi a ["temperatura"] !! e também tentando Cordas elenco para NSString - verificou as novas obras solução swiftlang.ng.bluemix.net/#/repl/57d3bc683a422409bf36c391
discorevilo
[String: String]não pode funcionar porque existem alguns valores numéricos. Não funciona em um Mac Playground
vadian
@ ah vadian, sim, eu não tinha notado aqueles e a caixa de areia rápida prestativamente escondeu-os! - Corrigido [mais uma vez] agora (e testado no macOS). Obrigado por apontar isto :)
discorevilo
6
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)")
}
BhuShan PaWar
fonte
5

Eu construí QuickType exatamente para esta finalidade. Basta colar seu JSON de amostra e o quicktype gera essa hierarquia de tipos para seus dados da 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
}

Ele também gera código de empacotamento livre de dependência para persuadir o valor de retorno de JSONSerialization.jsonObjectpara a Forecast, incluindo um construtor de conveniência que usa uma string JSON para que você possa analisar rapidamente um Forecastvalor fortemente digitado e acessar seus campos:

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

Você pode instalar o quicktype a partir do npm com npm i -g quicktypeou usar a interface da web para obter o código completo gerado para colar no seu playground.

David Siegel
fonte
Os links estão falhando.
empatou ..
Economizador de tempo fantástico! Bom trabalho!
Marko
1
Ferramenta brilhante. Obrigado.
Ahmet Ardal
4

Atualizado os isConnectToNetwork-Functiondepois, graças a este cargo .

Eu escrevi um método extra para isso:

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
}

Então agora você pode facilmente chamar isso em seu aplicativo onde quiser

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
    }
}
Marco Weber
fonte
Marco, qual seria a melhor maneira de adicionar essa função no projeto? em qualquer contorller ou modelo?
precisa saber é o seguinte
ei, você acabou de adicionar o primeiro método func loadingJSON (...) em um arquivo ou classe mais rápida. Depois que você é capaz de chamar isto de cada controlador em seu projeto
Marco Weber
Eu tentei isso, mas eu amo a idéia de demonstrar uma solução completa e também como usá-lo, incluindo o isConnectedToNetwork método auxiliar () me a ideia de como implementá-lo provavelmente em bom código
Amr irritado
então usei um novo arquivo swift (clique na esquerda da árvore do projeto, novo arquivo ..., arquivo swift) e chamei de jsonhelper.swift. nesse arquivo, você coloca o primeiro código, loadingJSON () e isConnectedToNetwork (). Depois disso, você pode usar essas duas funções em todas as partes do seu projeto. por exemplo, em um loginVC, como uma ação do botão de login, você pode usar o segundo código; obviamente, é necessário alterar o domínio, a sequência de postagem e os valores de paseJson (parseJSON ["loginSuccessfull"]) para que correspondam a seu arquivo php
Marco Weber
0

Swift tem uma inferência de tipo poderosa. Vamos nos livrar do clichê "if let" ou "guard let" e forçar o desembrulho usando a abordagem funcional:

  1. Aqui está o nosso JSON. Podemos usar JSON opcional ou usual. Estou usando opcional no nosso exemplo:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
  1. Funções auxiliares. Precisamos escrevê-los apenas uma vez e depois reutilizar com qualquer dicionário:

    /// 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. E aqui está a nossa mágica - extraia o valor:

    let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound

Apenas uma linha de código e sem forçar a abertura ou a conversão manual de tipos. Esse código funciona no playground, para que você possa copiá-lo e verificá-lo. Aqui está uma implementação no GitHub.

J. Doe
fonte
0

Essa é outra maneira de resolver seu problema. Então, confira a solução abaixo. Espero que ajude você.

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)")
}
Olcay Ertaş
fonte
0

O problema está no método de interação da API. A análise JSON é alterada apenas na sintaxe. O principal problema está na maneira de buscar dados. O que você está usando é uma maneira síncrona de obter dados. Isso não funciona em todos os casos. O que você deve usar é uma maneira assíncrona de buscar dados. Dessa forma, você deve solicitar dados por meio da API e aguardar a resposta dos dados. Você pode conseguir isso com sessões de URL e bibliotecas de terceiros como Alamofire. Abaixo está o código para o método de sessão de 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()
Arun K
fonte
0
{
    "User":[
      {
        "FirstUser":{
        "name":"John"
        },
       "Information":"XY",
        "SecondUser":{
        "name":"Tom"
      }
     }
   ]
}

Se eu criar um modelo usando o json anterior Usando este link [blog]: http://www.jsoncafe.com para gerar estrutura Codable ou Qualquer Formato

Modelo

import Foundation
struct RootClass : Codable {
    let user : [Users]?
    enum CodingKeys: String, CodingKey {
        case user = "User"
    }

    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        user = try? values?.decodeIfPresent([Users].self, forKey: .user)
    }
}

struct Users : Codable {
    let firstUser : FirstUser?
    let information : String?
    let secondUser : SecondUser?
    enum CodingKeys: String, CodingKey {
        case firstUser = "FirstUser"
        case information = "Information"
        case secondUser = "SecondUser"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        firstUser = try? FirstUser(from: decoder)
        information = try? values?.decodeIfPresent(String.self, forKey: .information)
        secondUser = try? SecondUser(from: decoder)
    }
}
struct SecondUser : Codable {
    let name : String?
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
    }
}
struct FirstUser : Codable {
    let name : String?
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
    }
}

Analisar

    do {
        let res = try JSONDecoder().decode(RootClass.self, from: data)
        print(res?.user?.first?.firstUser?.name ?? "Yours optional value")
    } catch {
        print(error)
    }
Abishek T
fonte