Правильно разбор JSON в Swift 3
Я пытаюсь получить ответ JSON и сохранить результаты в переменной. У меня были версии этого кода в предыдущих выпусках Swift, пока не была выпущена GM-версия Xcode 8. Я взглянул на несколько похожих постов по Stackru: Swift 2 Parsing JSON - Невозможно подписать значение типа 'AnyObject' и JSON Parsing в Swift 3.
Однако, похоже, что идеи, изложенные там, не применимы в этом сценарии.
Как правильно разобрать ответ JSON в Swift 3? Что-то изменилось в способе чтения JSON в Swift 3?
Ниже приведен код (его можно запустить на детской площадке):
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)")
}
}
}
Изменить: Вот пример результатов из вызова API после 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]
11 ответов
Прежде всего, никогда не загружайте данные синхронно с удаленного URL, всегда используйте асинхронные методы, такие как URLSession
,
"Любой" не имеет подписчиков
происходит потому, что компилятор понятия не имеет, к какому типу относятся промежуточные объекты (например, currently
в ["currently"]!["temperature"]
) и так как вы используете типы коллекции Foundation, такие как NSDictionary
компилятор понятия не имеет о типе.
Кроме того, в Swift 3 требуется сообщить компилятору о типе всех подписанных объектов.
Вы должны привести результат сериализации JSON к фактическому типу.
Этот код использует URLSession
и исключительно 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()
Чтобы напечатать все пары ключ / значение currentConditions
ты мог бы написать
let currentConditions = parsedData["currently"] as! [String:Any]
for (key, value) in currentConditions {
print("\(key) - \(value) ")
}
Примечание относительно jsonObject(with data
:
Многие (кажется, все) учебники предлагают .mutableContainers
или же .mutableLeaves
варианты что это полная ерунда в Swift. Эти два параметра являются устаревшими параметрами Objective C, чтобы назначить результат NSMutable...
объекты. В свифт любой var
iable является изменяемым по умолчанию и передает любую из этих опций и присваивает результат let
Константа не имеет никакого эффекта вообще. Кроме того, большинство реализаций никогда не изменяют десериализованный JSON.
Единственный (редкий) вариант, который полезен в Swift, это .allowFragments
что требуется, если корневой объект JSON может иметь тип значения (String
, Number
, Bool
или же null
), а не один из типов коллекции (array
или же dictionary
). Но обычно опускаем options
параметр, который означает, что нет параметров.
================================================== =========================
Некоторые общие соображения для разбора JSON
JSON - это хорошо организованный текстовый формат. Читать строку JSON очень легко. Внимательно прочитайте строку. Существует только шесть различных типов - два типа коллекций и четыре типа значений.
Типы коллекций
- Array - JSON: объекты в квадратных скобках
[]
- Свифт:[Any]
но в большинстве случаев[[String:Any]]
- Словарь - JSON: объекты в фигурных скобках
{}
- Свифт:[String:Any]
Типы значений
- Строка - JSON: любое значение в двойных кавычках
"Foo"
, четное"123"
или же"false"
- Свифт:String
- Число - JSON: числовые значения не в двойных кавычках
123
или же123.0
- Свифт:Int
или жеDouble
- Bool - JSON:
true
или жеfalse
не в двойных кавычках - Swift:true
или жеfalse
- нуль - JSON:
null
- Свифт:NSNull
Согласно спецификации JSON все ключи в словарях должны быть String
,
В основном, всегда рекомендуется использовать дополнительные привязки, чтобы безопасно развернуть дополнительные
Если корневым объектом является словарь ({}
) приведите тип к [String:Any]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...
и получить значения по ключам с помощью (OneOfSupportedJSONTypes
является либо коллекцией JSON, либо типом значения, как описано выше.)
if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
print(foo)
}
Если корневой объект является массивом ([]
) приведите тип к [[String:Any]]
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...
и перебрать массив с
for item in parsedData {
print(item)
}
Если вам нужен элемент с определенным индексом, проверьте также, существует ли индекс
if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
let item = parsedData[2] as? OneOfSupportedJSONTypes {
print(item)
}
}
В редком случае, когда JSON - это просто один из типов значений, а не тип коллекции, вы должны передать .allowFragments
вариант и приведите результат к соответствующему типу значения, например
if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...
Apple опубликовала исчерпывающую статью в блоге Swift: Работа с JSON в Swift
Обновление: в Swift 4+ Codable
Протокол предоставляет более удобный способ разбора JSON непосредственно на структуры / классы.
Большое изменение, которое произошло с Xcode 8 Beta 6 для Swift 3, заключалось в том, что id теперь импортируется как Any
скорее, чем AnyObject
,
Это означает, что parsedData
возвращается в виде словаря, скорее всего, с типом [Any:Any]
, Без использования отладчика я не смог бы сказать вам точно, что ваш каст NSDictionary
будет делать, но ошибка, которую вы видите, потому что dict!["currently"]!
имеет тип Any
Итак, как вы решаете это? Судя по тому, как вы на него ссылались dict!["currently"]!
это словарь и поэтому у вас есть много вариантов:
Сначала вы можете сделать что-то вроде этого:
let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]
Это даст вам объект словаря, который вы затем сможете запросить значения, и вы можете получить свою температуру следующим образом:
let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double
Или, если вы предпочитаете, вы можете сделать это в строке:
let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double
Надеюсь, это поможет, боюсь, у меня не было времени написать пример приложения для его тестирования.
И последнее замечание: проще всего сделать простое приведение полезной нагрузки JSON в [String: AnyObject]
в самом начале.
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)")
}
После этогообновил функцию isConnectToNetwork, благодаря этому сообщению. Проверьте подключение к Интернету с помощью Swift.
я написал дополнительный метод для этого:
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
}
Так что теперь вы можете легко вызвать это в вашем приложении, где вы хотите
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
}
}
Я построил quicktype именно для этой цели. Просто вставьте образец JSON, и quicktype создаст иерархию типов для данных 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
}
Он также генерирует беспошлинный код маршалинга для получения возвращаемого значения JSONSerialization.jsonObject
в Forecast
включая вспомогательный конструктор, который принимает строку JSON, чтобы вы могли быстро проанализировать строго типизированный Forecast
Значение и доступ к его полям:
let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)
Вы можете установить quicktype из npm с помощью npm i -g quicktype
или используйте веб-интерфейс, чтобы получить полный сгенерированный код для вставки на игровую площадку.
Это еще один способ решить вашу проблему. Пожалуйста, ознакомьтесь с решением ниже. Надеюсь, это поможет тебе.
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)")
}
2023 | СВИФТ 5.1 | Результаты решения
Образец данных:
// Codable is important to be able to encode/decode from/to JSON!
struct ConfigCreds: Codable {
// some params
}
Пример использования решения:
var configCreds = ConfigCreds()
var jsonStr: String = ""
// get JSON from Object
configCreds
.asJson()
.onSuccess { jsonStr = $0 }
.onFailure { _ in // any failure code }
// get object of type "ConfigCreds" from JSON
someJsonString
.decodeFromJson(type: ConfigCreds.self)
.onSuccess { configCreds = $0 }
.onFailure { _ in // any failure code }
Серверный код:
@available(macOS 10.15, *)
public extension Encodable {
func asJson() -> Result<String, Error>{
JSONEncoder()
.try(self)
.flatMap{ $0.asString() }
}
}
public extension String {
func decodeFromJson<T>(type: T.Type) -> Result<T, Error> where T: Decodable {
self.asData()
.flatMap { JSONDecoder().try(type, from: $0) }
}
}
///////////////////////////////
/// HELPERS
//////////////////////////////
@available(macOS 10.15, *)
fileprivate extension JSONEncoder {
func `try`<T : Encodable>(_ value: T) -> Result<Output, Error> {
do {
return .success(try self.encode(value))
} catch {
return .failure(error)
}
}
}
fileprivate extension JSONDecoder {
func `try`<T: Decodable>(_ t: T.Type, from data: Data) -> Result<T,Error> {
do {
return .success(try self.decode(t, from: data))
} catch {
return .failure(error)
}
}
}
fileprivate extension String {
func asData() -> Result<Data, Error> {
if let data = self.data(using: .utf8) {
return .success(data)
} else {
return .failure(WTF("can't convert string to data: \(self)"))
}
}
}
fileprivate extension Data {
func asString() -> Result<String, Error> {
if let str = String(data: self, encoding: .utf8) {
return .success(str)
} else {
return .failure(WTF("can't convert Data to string"))
}
}
}
fileprivate func WTF(_ msg: String, code: Int = 0) -> Error {
NSError(code: code, message: msg)
}
internal extension NSError {
convenience init(code: Int, message: String) {
let userInfo: [String: String] = [NSLocalizedDescriptionKey:message]
self.init(domain: "FTW", code: code, userInfo: userInfo)
}
}
Swift имеет мощный тип вывода. Позволяет избавиться от шаблонов "если позволено" или "охранять пусть" и принудительно разворачивает, используя функциональный подход:
- Вот наш JSON. Мы можем использовать необязательный JSON или обычный. Я использую необязательный в нашем примере:
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
Всего одна строка кода и никаких принудительных развёрток или ручного приведения типов. Этот код работает на детской площадке, поэтому вы можете скопировать и проверить его. Вот реализация на GitHub.
Swift 5 Не могу получить данные из вашего API. Самый простой способ разобрать json - использовать
Decodable
протокол. Или же
Codable
(
Encodable & Decodable
). Например:
let json = """
{
"dueDate": {
"year": 2021,
"month": 2,
"day": 17
}
}
"""
struct WrapperModel: Codable {
var dueDate: DueDate
}
struct DueDate: Codable {
var year: Int
var month: Int
var day: Int
}
let jsonData = Data(json.utf8)
let decoder = JSONDecoder()
do {
let model = try decoder.decode(WrapperModel.self, from: jsonData)
print(model)
} catch {
print(error.localizedDescription)
}
{
"User":[
{
"FirstUser":{
"name":"John"
},
"Information":"XY",
"SecondUser":{
"name":"Tom"
}
}
]
}
Если я создаю модель с использованием предыдущего json. Используя эту ссылку [блог]: http://www.jsoncafe.com/ для создания структуры Codable или Any Format
Модель
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)
}
}
Разобрать
do {
let res = try JSONDecoder().decode(RootClass.self, from: data)
print(res?.user?.first?.firstUser?.name ?? "Yours optional value")
} catch {
print(error)
}
Проблема связана с методом взаимодействия API. Разбор JSON изменяется только в синтаксисе. Основная проблема заключается в способе получения данных. То, что вы используете, - это синхронный способ получения данных. Это не работает в каждом случае. То, что вы должны использовать, - это асинхронный способ получения данных. Таким образом, вы должны запрашивать данные через API и ждать, пока он ответит данными. Вы можете добиться этого с помощью URL сессии и сторонних библиотек, таких как Alamofire. Ниже приведен метод "Код для 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()