Poprawne parsowanie JSON w Swift 3

Próbuję pobrać odpowiedź JSON i zapisać wyniki w zmiennej. Miałem wersje tego kodu pracy w poprzednich wydaniach Swift, aż GM wersja Xcode 8 został wydany. Spojrzałem na kilka podobnych postów na StackOverflow: Swift 2 Parsing JSON - Cannot subscript a value of type 'AnyObject' and JSON Parsing in Swift 3 .

Wydaje się jednak, że przedstawione tam pomysły nie mają zastosowania w tym scenariuszu.

Jak poprawnie parsować JSON reponse w Swift 3? Czy coś się zmieniło w sposobie odczytu JSON w Swift 3?

Poniżej znajduje się kod (można go uruchomić na Placu Zabaw):

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: Oto Przykładowe wyniki wywołania API po 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]
Author: Community, 2016-09-10

6 answers

Po pierwsze nigdy nie ładuj danych synchronicznie ze ZDALNEGO ADRESU URL , używaj zawsze metod asynchronicznych, takich jak URLSession.

'Any' nie ma członków indeksu

Występuje, ponieważ kompilator nie ma pojęcia, jakiego typu są obiekty pośrednie (na przykład currently w ["currently"]!["temperature"]), a ponieważ używasz podstawowych typów kolekcji, takich jak NSDictionary, kompilator nie ma pojęcia o tym typie.

Dodatkowo w Swift 3 wymagane jest poinformowanie kompilatora o typie wszystkich subscripted objects.

Musisz oddać wynik serializacji JSON do rzeczywistego typu.

Ten kod używa URLSession i wyłącznie typów Swift native

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()

Aby wydrukować wszystkie pary klucz / wartość currentConditions możesz napisać

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

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

Uwaga dotycząca jsonObject(with data:

Wiele (wydaje się, że wszystkie) samouczków sugeruje .mutableContainers lub .mutableLeaves opcje, co jest całkowicie nonsensem w języku Swift. The two opcje są starszymi opcjami Objective-C, aby przypisać wynik do obiektów NSMutable.... W języku Swift każda var iable jest domyślnie zmienna, a przekazanie dowolnej z tych opcji i przypisanie wyniku do stałej let nie ma żadnego wpływu. Ponadto większość implementacji i tak nigdy nie mutuje deserializowanego JSON.

Jedyną (rzadką) opcją przydatną w języku Swift jest .allowFragments, która jest wymagana, jeśli obiekt JSON root może być typem wartości(String, Number, Bool lub null) zamiast jeden z typów kolekcji (array lub dictionary). Ale zwykle pomija parametr options, co oznacza Brak opcji .

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

Niektóre ogólne uwagi do analizy JSON

JSON jest dobrze ułożonym formatem tekstowym. Bardzo łatwo odczytać ciąg JSON. przeczytaj uważnie tekst . Istnieje tylko sześć różnych typów-dwa typy kolekcji i cztery typy wartości.


The typy kolekcji to

  • Array - JSON: obiekty w nawiasach kwadratowych [] - Swift: [Any] ale w większości przypadków [[String:Any]]
  • Słownik - JSON: obiekty w nawiasach klamrowych {} - Swift: [String:Any]

Typami wartości są

  • String - JSON: dowolna wartość w podwójnych cudzysłowach "Foo", nawet "123" lub "false" – Swift: String
  • liczba - JSON: wartości liczbowe Nie {[56] } w podwójnych cudzysłowach 123 or 123.0 - Swift: Int or Double
  • Bool - JSON: true lub false Nie w podwójnych cudzysłowach-Swift: true lub false
  • null - JSON: null - Swift: NSNull

Zgodnie ze specyfikacją JSON wszystkie klucze w słownikach muszą być String.


W zasadzie zawsze zaleca się używanie opcjonalnych wiązań do bezpiecznego rozpakowywania opcji

Jeśli obiekt root jest słownikiem ({}) Wrzuć typ do [String:Any]

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

I pobierać wartości za pomocą kluczy z (OneOfSupportedJSONTypes jest albo kolekcją JSON, albo typem wartości, jak opisano powyżej.)

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

Jeśli obiekt root jest tablicą ([]) rzuć typ na [[String:Any]]

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

I iteracja przez tablicę z

for item in parsedData {
    print(item)
}

Jeśli potrzebujesz pozycji w określonym indeksie sprawdź również, czy indeks istnieje

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

W rzadkim przypadku, że JSON jest po prostu jednym z typów wartości – a nie typ kolekcji – musisz przekazać opcję .allowFragments i oddać wynik do odpowiedniego typu wartości na przykład

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

Apple opublikowało obszerny artykuł na blogu Swift: praca z JSON w języku Swift
 146
Author: vadian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-20 10:12:23

Dużą zmianą, która nastąpiła w Xcode 8 Beta 6 dla Swift 3 było to, że id importuje teraz jako Any, a nie AnyObject.

Oznacza to, że parsedData jest zwracany jako słownik najprawdopodobniej z typem [Any:Any]. Bez użycia debuggera nie mogłem dokładnie powiedzieć, co zrobi twój cast do NSDictionary, ale błąd, który widzisz, jest ponieważ dict!["currently"]! mA typ Any

Więc, jak to rozwiązać? Z tego, jak się do niego odnosisz, zakładam, że dict!["currently"]! jest słownikiem, więc masz wiele opcje:

Najpierw mógłbyś zrobić coś takiego:

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

To da ci obiekt słownika, który możesz następnie odpytywać o wartości, dzięki czemu możesz uzyskać temperaturę w następujący sposób:

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

Lub jeśli wolisz, możesz to zrobić w linii:

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

Mam nadzieję, że to pomoże, obawiam się, że nie miałem czasu, aby napisać przykładową aplikację, aby ją przetestować.

Ostatnia Uwaga: najprostszą rzeczą do zrobienia, może być po prostu wrzucenie ładunku JSON do [String: AnyObject] tuż przy początek.

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
 12
Author: discorevilo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-10 08:09:19
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
Author: BhuShan PaWar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-24 05:19:51

Zaktualizowano funkcję isConnectToNetwork, dzięki temu postowi Sprawdź połączenie z Internetem za pomocą Swift

Napisałem do tego dodatkową metodę:

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

}

Więc teraz możesz łatwo zadzwonić do tego w aplikacji, gdzie chcesz

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
Author: Marco Weber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:41

Zbudowałem quicktype właśnie w tym celu. Wystarczy wkleić przykładowy JSON i quicktype wygeneruje tę hierarchię typów dla danych 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
}

Generuje również wolny od zależności kod marshaling, aby koncentrować wartość zwracaną JSONSerialization.jsonObject na Forecast, w tym Wygodny konstruktor, który pobiera ciąg JSON, dzięki czemu można szybko przetworzyć silnie wpisaną wartość Forecast i uzyskać dostęp do jej pól:

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

Możesz zainstalować quicktype z npm za pomocą npm i -g quicktype lub użyj web UI aby uzyskać kompletny wygenerowany kod do wklejenia na swoim placu zabaw.

 3
Author: David Siegel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-30 22:17:05

Problem polega na metodzie interakcji API.Parsowanie JSON jest zmieniane tylko w składni. Głównym problemem jest sposób pobierania danych. To, czego używasz, to synchroniczny sposób uzyskiwania danych. To nie działa w każdym przypadku. Powinieneś używać asynchronicznego sposobu pobierania danych. W ten sposób musisz zażądać danych za pośrednictwem API i poczekać, aż odpowie na dane. Możesz to osiągnąć dzięki sesji URL i bibliotekom innych firm, takim jak Alamofire. Poniżej znajduje się kod dla Metoda sesji 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()
 0
Author: Arun K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-27 14:16:00