在Swift中使用JSON

作者: 光明程辉 | 来源:发表于2018-07-07 13:00 被阅读10次

如果你使用过Objective-C 解析JSON, 那么你一定对 NSJSONSerialization 并不陌生。 它的总体步骤大致是这样:

1、先从 Data 对象中解析出 NSDictionary 或 NSArray,。
2、然后在从这里面按照属性名称取出需要的值。
3、最后再用这些值给实体对象赋值。

如果你的应用程序与Web应用程序通信,则从服务器返回的信息通常格式为JSON。您可以使用Foundation框架的JSONSerialization类将JSON转换为Swift数据类型:

如Dictionary,Array,String,Number和Bool。

但是,由于你无法确定应用程序接收的JSON的结构或值,因此正确反序列化模型对象可能具有挑战性。这就是我接下来介绍如何在应用程序中使用JSON时可以采取的一些方法。

从JSON中提取值

该JSONSerialization类方法的JSONObject(附:选项:)返回类型的值的任何和引发错误,如果数据不能被解析。

import Foundation

let data: Data // received from a network request, for example
let json = try? JSONSerialization.jsonObject(with: data, options: [])

尽管有效的JSON 可能只包含单个值,但Web应用程序的响应通常会将对象或数组编码为 顶级对象。
你可以使用可选的绑定和as?在if或guard语句中键入cast运算符,以将已知类型的值提取为常量。
要从JSON对象类型获取Dictionary值,请将其有条件地转换为[String:Any]。
要从JSON数组类型获取Array值,请将其有条件地转换为[Any](或具有更具体元素类型的数组,如[String])。你可以使用带有下标访问器的类型转换可选绑定或带枚举的模式匹配,按键或按索引提取数组值来提取字典值。

  // Example JSON with object root:
/*
{
    "someKey": 42.0,
    "anotherKey": {
        "someNestedKey": true
    }
}
*/
if let dictionary = jsonWithObjectRoot as? [String: Any] {
if let number = dictionary["someKey"] as? Double {
    // access individual value in dictionary
}

for (key, value) in dictionary {
    // access all key / value pairs in dictionary
}

if let nestedDictionary = dictionary["anotherKey"] as? [String: Any] {
    // access nested dictionary values by key
  }
}

// Example JSON with array root:
/*
[
    "hello", 3, true
]
*/
if let array = jsonWithArrayRoot as? [Any] {
if let firstObject = array.first {
    // access individual object in array
}

for object in array {
    // access all objects in array
}

for case let string as String in array {
    // access only string values in array
  }
}

Swift的内置语言功能可以轻松安全地提取和使用使用Foundation API解码的JSON数据 - 无需外部库或框架。当然你使用第三方框架也是可以的,效率会高些。或者你自己封装一个也行。

从JSON中提取的值创建模型对象

由于大多数Swift应用程序遵循模型 - 视图 - 控制器设计模式,因此在模型定义中将JSON数据转换为特定于应用程序域的对象通常很有用。这也就是为什么 移动开发、前端开发、后台开发等都采用该方式的原因了。

例如,在编写为本地餐馆提供搜索结果的应用程序时,您可以实现一个带有初始化程序的Restaurant模型,该初始化程序接受JSON对象和向服务器/搜索端点发出HTTP请求的类型方法,然后异步返回一个数组中 Restaurant 的对象。

请思考以下 Restaurant 模型:

import Foundation

struct Restaurant {
enum Meal: String {
    case breakfast, lunch, dinner
}

let name: String
let location: (latitude: Double, longitude: Double)
let meals: Set<Meal>
}

甲 Restaurant 具有名称类型的字符串,一个位置表示为坐标对,和一个集的膳食含有嵌套的值膳食枚举。

以下是一个如何在服务器响应中表示单个Restaurant的示例:

{
"name": "美味餐馆",
"coordinates": {
    "lat": 37.330576,
    "lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
}

编写可选的JSON初始化程序

要从JSON表示转换为Restaurant对象,请编写一个初始化程序,该初始化程序接受一个Any参数,该参数将JSON表示中的数据提取并转换为属性。

extension Restaurant {
init?(json: [String: Any]) {
    guard let name = json["name"] as? String,
        let coordinatesJSON = json["coordinates"] as? [String: Double],
        let latitude = coordinatesJSON["lat"],
        let longitude = coordinatesJSON["lng"],
        let mealsJSON = json["meals"] as? [String]
    else {
        return nil
    }

    var meals: Set<Meal> = []
    for string in mealsJSON {
        guard let meal = Meal(rawValue: string) else {
            return nil
        }

        meals.insert(meal)
    }

    self.name = name
    self.coordinates = (latitude, longitude)
    self.meals = meals
}
}

在上面的示例中,使用可选绑定将每个值从传递的JSON字典提取为常量,并将as?转成可选类型。对于name属性,提取的名称值只是按原样分配。对于坐标属性,提取的纬度和经度值在分配之前被组合成元组。对于饭菜的属性,所提取的字符串值遍历构建一个集的膳食枚举值。

编写带错误处理的JSON初始化程序

前面的示例实现了一个可选的初始值设定项,如果反序列化失败,则返回nil。或者,你可以定义符合Error协议的类型,并实现一个初始化程序,只要反序列化失败,就会抛出该类型的错误。

enum SerializationError: Error {
case missing(String)
case invalid(String, Any)
}

extension Restaurant {
init(json: [String: Any]) throws {
    // 提取 name
    guard let name = json["name"] as? String else {
        throw SerializationError.missing("name")
    }

    // 提取 and 验证 coordinates
    guard let coordinatesJSON = json["coordinates"] as? [String: Double],
        let latitude = coordinatesJSON["lat"],
        let longitude = coordinatesJSON["lng"]
    else {
        throw SerializationError.missing("coordinates")
    }

    let coordinates = (latitude, longitude)
    guard case (-90...90, -180...180) = coordinates else {
        throw SerializationError.invalid("coordinates", coordinates)
    }

    // 提取 and 验证 meals
    guard let mealsJSON = json["meals"] as? [String] else {
        throw SerializationError.missing("meals")
    }

    var meals: Set<Meal> = []
    for string in mealsJSON {
        guard let meal = Meal(rawValue: string) else {
            throw SerializationError.invalid("meals", string)
        }

        meals.insert(meal)
    }

    // 初始化 属性
    self.name = name
    self.coordinates = coordinates
    self.meals = meals
}

}

这里,Restaurant类型声明了一个嵌套的SerializationError类型,该类型定义了枚举案例,其中包含缺失或无效属性的关联值。在JSON初始化程序的抛出版本中,不是通过返回nil来指示失败,而是抛出错误来传达特定的失败。此版本还执行输入数据的验证,以确保坐标表示有效地域的坐标对,并且每个为名称的膳食在JSON指定对应于膳食枚举的情况。

编写用于获取结果的类型方法

Web应用程序端点通常在单个JSON响应中返回多个资源。例如,/搜索端点可以返回与所请求的查询参数匹配的零个或多个餐馆,并包括这些表示以及其他元数据,下面这种 JSON 很常见:

{
"query": "sandwich",
"results_count": 12,
"page": 1,
"results": [
    {
        "name": "Caffè Macs",
        "coordinates": {
            "lat": 37.330576,
            "lng": -122.029739
        },
        "meals": ["breakfast", "lunch", "dinner"]
    },
    ...
]
}

你可以在Restaurant结构上创建一个类型方法,将查询方法参数转换为相应的请求对象,并将HTTP请求发送到Web服务。此代码还负责处理响应,反序列化JSON数据,从“results”数组中的每个提取的字典创建Restaurant对象,并在完成处理程序中异步返回它们。

extension Restaurant {
private let urlComponents: URLComponents // base URL components of the web service
private let session: URLSession // shared session for interacting with the web service

static func restaurants(matching query: String, completion: ([Restaurant]) -> Void) {
    var searchURLComponents = urlComponents
    searchURLComponents.path = "/search"
    searchURLComponents.queryItems = [URLQueryItem(name: "q", value: query)]
    let searchURL = searchURLComponents.url!

    session.dataTask(url: searchURL, completion: { (_, _, data, _)
        var restaurants: [Restaurant] = []

        if let data = data,
            let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
            for case let result in json["results"] {
                if let restaurant = Restaurant(json: result) {
                    restaurants.append(restaurant)
                }
            }
        }

        completion(restaurants)
    }).resume()
}

}

当用户在搜索栏中输入文本以使用匹配的餐馆填充表格视图时,视图控制器可以调用此方法:

import UIKit

extension ViewController: UISearchResultsUpdating {
func updateSearchResultsForSearchController(_ searchController: UISearchController) {
    if let query = searchController.searchBar.text, !query.isEmpty {
        Restaurant.restaurants(matching: query) { restaurants in
            self.restaurants = restaurants
            self.tableView.reloadData()
        }
    }
}

}

以这种方式分离关注点提供了用于从视图控制器访问餐馆资源的一致界面,即使在关于web服务的实现细节改变时也是如此。前端开发也是如此。

如果有兴趣请看:

用 Codable 协议实现快速 JSON 解析

最后给个API 给大家练习

https://api.douban.com//v2/movie/in_theaters

相关文章

网友评论

    本文标题:在Swift中使用JSON

    本文链接:https://www.haomeiwen.com/subject/zjqwuftx.html