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]
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
or123.0
- Swift:Int
orDouble
-
Bool - JSON:
true
lubfalse
Nie w podwójnych cudzysłowach-Swift:true
lubfalse
-
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
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>
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)")
}
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
}
}
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.
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()
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