在Swift 3中正确解析JSON


123

我正在尝试获取JSON响应并将结果存储在变量中。在Xcode 8的GM版本发布之前,我已经在Swift的早期版本中使用了此代码的版本。我在StackOverflow上看到了一些类似的文章:Swift 2解析JSON-无法在Swift 3中下标'AnyObject'类型的值JSON解析

但是,似乎此处传达的想法不适用于这种情况。

如何在Swift 3中正确解析JSON响应?在Swift 3中读取JSON的方式是否有所改变?

下面是有问题的代码(可以在操场上运行):

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]

您可以放入从API调用返回的示例数据吗?
用户

1
是的,我只是添加了print(currentConditions)之后打印的结果样本。希望能帮助到你。
user2563039 '16

Answers:


172

首先,永远不要从远程URL同步加载数据,而应始终使用异步方法,例如URLSession

'Any'没有下标成员

是因为编译器没有什么类型的中间对象(例如理念currently["currently"]!["temperature"]),并且由于使用的是基金会收藏类型,如NSDictionary编译器在所有有关的类型不知道。

另外,在Swift 3中,需要通知编译器所有下标对象的类型。

您必须将JSON序列化的结果转换为实际类型。

此代码的用途URLSession专门斯威夫特本地类型

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...对象。在Swift中var,默认情况下任何变量都是可变的,并且传递这些选项中的任何一个并将结果分配给let常量都完全无效。此外,大多数实现绝不会改变反序列化的JSON。

唯一的(罕见)选项,在夫特是有用是.allowFragments如果如果JSON根对象可以是一个值类型是必需(StringNumberBoolnull)而不是集合类型中的一个(arraydictionary)。但通常会省略options表示No options的参数。

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

解析JSON的一些一般注意事项

JSON是一种排列合理的文本格式。读取JSON字符串非常容易。仔细阅读字符串。只有六种不同的类型–两种收集类型和四种值类型。


收集类型为

  • 数组 -JSON:方括号中的对象[]-Swift:[Any]但在大多数情况下[[String:Any]]
  • 字典 -JSON:大括号中的对象{}-Swift:[String:Any]

值类型为

  • 字符串 -JSON:双引号中的任何值"Foo",偶数"123""false"– Swift:String
  • 数字 -JSON:数值不带双引号123123.0– Swift:IntDouble
  • 布尔 -JSON:truefalse 使用双引号-Swift:truefalse
  • null -JSON:null– Swift: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 { ...

苹果在Swift博客中发表了一篇详尽的文章:在Swift中使用JSON


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

在Swift 4+中,该Codable协议提供了一种将JSON直接解析为结构/类的更便捷的方法。

例如问题中的给定JSON示例(稍作修改)

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

可以解码为struct Weather。Swift类型与上述相同。还有一些其他选项:

  • 表示的字符串URL可以直接解码为URL
  • time整数可以被解码为Date与所述dateDecodingStrategy .secondsSince1970
  • snaked_cased JSON键可被转化为驼峰keyDecodingStrategy .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)
}

其他可编码来源:


这非常有帮助。我很好奇,为什么上面的代码在操场上运行时没有显示可见的输出。
user2563039 '16

如上所述,您需要将歧义结果dict!["currently"]!转换为字典,编译器可以安全地推断出后续的键预订。
vadian

1
苹果的文章很酷,但是由于某种原因我无法使其与Swift 3一起运行。它抱怨类型[String:任何]?没有下标成员。它也存在其他一些问题,但是我能够解决它。任何人都有其实际运行的代码示例吗?
Shades

@Shades提出问题并发布您的代码。您的问题很可能与展开的可选项目有关。
vadian

您可以放入从API调用返回的示例数据吗?
用户

12

Swift 3的Xcode 8 Beta 6发生的一个大变化是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

希望这会有所帮助,恐怕我还没有时间编写示例应用程序来对其进行测试。

最后一点:最简单的操作可能是[String: AnyObject]在开始时就将JSON有效负载直接转换为正确的位置。

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>

dict!["currently"]! as! [String: String]将会崩溃
vadian

它的崩溃点是[“温度”]!并且还尝试将String转换为NSString-验证了新解决方案的工作原理swiftlang.ng.bluemix.net/#/repl/57d3bc683a422409bf36c391
discorevilo

[String: String]根本无法工作,因为有几个数字值。在Mac Playground中
不起作用

@vadian啊,是的,我没有注意到这些,而迅捷的沙箱有效地将它们藏起来了!-现在[再次一次]更正(并在macOS上进行了测试)。感谢您指出:)
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)")
}

5

我正是为此目的而构建了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.jsonObjectForecast,其中包括使用JSON字符串的便捷构造函数,以便您可以快速解析强类型Forecast值并访问其字段:

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

您可以使用 npm从npm安装quicktype npm i -g quicktype使用Web UI将完整的生成代码粘贴到您的游乐场。


链接失败。
提请..

神奇的时间节省!做得好!
marko

1
出色的工具。谢谢。
Ahmet Ardal

4

isConnectToNetwork-Function之后更新了,这要归功于这篇文章

我为此写了一个额外的方法:

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

Marco,在项目中添加此功能的最佳方法是什么?在任何控制器或模型中?
KamalPanhwar

嘿,您只需在一个额外的swift文件或类中添加第一个方法func loadingJSON(...)。之后,您可以从项目中的每个控制器调用此命令
Marco Weber

我确实尝试过,但是我喜欢演示完整的解决方案以及如何使用它(包括帮助程序方法isConnectedToNetwork())的想法,它使我想到了如何以良好的代码实现它的想法
Amr Angry

所以我只是使用了一个新的swift文件(在项目树上左击,新文件...,swift文件),并将其命名为jsonhelper.swift。在此文件中,您放置第一个代码,即loadingJSON()和isConnectedToNetwork()。之后,您可以在项目的每个部分中使用这两个功能。例如,在loginVC中,作为登录按钮的操作,您可以使用第二个代码,显然您必须更改域,发布字符串和paseJson值(parseJSON [“ loginSuccessfull”]),以便它们与您的php文件
Marco Weber

0

Swift具有强大的类型推断功能。让我们摆脱“ if let”或“ guard let”样板,并使用功能性方法强制展开:

  1. 这是我们的JSON。我们可以使用可选的JSON或常规的。我在我们的示例中使用了可选的:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
  1. 辅助功能。我们只需要编写一次,然后再与任何字典重用:

    /// 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. 这是我们的魔力-提取价值:

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

只需一行代码,无需进行任何强制包装或手动强制类型转换。该代码在操场上可用,因此您可以复制并检查它。这是GitHub上的实现


0

这是解决问题的另一种方法。因此,请查看以下解决方案。希望对您有所帮助。

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)")
}

0

问题出在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()

0
{
    "User":[
      {
        "FirstUser":{
        "name":"John"
        },
       "Information":"XY",
        "SecondUser":{
        "name":"Tom"
      }
     }
   ]
}

如果我使用以前的json创建模型,则使用此链接[blog]:http : //www.jsoncafe.com生成可编码结构或任何格式

模型

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)
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.