(点击上方公众号,可快速关注)
来源:伯乐在线 - 古鲁伊
原文:developer.apple.com
译文:http://ios.jobbole.com/90465/
相比于主流的 Swift JSON 库使用自动映射来处理 JSON 转模型,苹果官方反而鼓励使用简单直接的方式。这也是我一直在用的。 阅读原文 »
Swift 中的 JSON 解析
如果 app 与 web 应用通信,那么从服务端返回的消息通常是 JSON 格式的。Foundation 框架的 JSONSerialization 类可以将 JSON 转换为 Swift 的 Dictionary、Array、String、Number和 Bool 等数据类型。但是因为无法确定 app 接收的 JSON 结构或值,所以很难正确地反序列化对象模型。本文列举了一些 app 中使用 JSON 的方法。
从 JSON 取值
JSONSerialization 类方法 jsonObject(with:options:) 可以返回 Any 类型的值,如果数据无法解析则会抛出 error。
import Foundation
let data: Data // received from a network request, for example
let json = try? JSONSerialization.jsonObject(with: data, options: [])
即使有效的 JSON 可能只包含一个简单的值,web 应用的响应结果也通常会将 object 或 array 编码,作为顶层对象。在 if 或 guard 语句中使用可选绑定和 as? 类型强制转换运算符可以将未知类型的值提取为常量。要从一个 JSON 对象类型获得 Dictionary 值,可以按条件强制转换为 [String: Any]。要从 JSON 数组类型获得 Array 值,可以按条件将其强制转换为 [Any](或元素类型更具体的数组,比如 [String])。使用非强制绑定下标访问器或枚举模式匹配的类型强制转换,可以用 key 获取 dictionary 值或用 index 获取 array 值。
// 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’s built-in language features make it easy to safely extract and work with JSON data decoded with Foundation APIs — without the need for an external library or framework.
Swift 固有语言的特点使得安全获取和使用 Foundation API(无需额外的库或框架)解码的 JSON 数据很容易
根据 JSON 获取到的值创建模型对象
大部分 Swift app 都遵循 Model-View-Controller 设计模式,所以经常将 JSON 数据转化为模型定义中针对 app 域名的具体对象。
例如,当编写一个 app,提供当地餐馆的搜索结果时,可能会用接收 JSON 对象的初始化式,和向服务器的 /search 终端发起 HTTP 请求,之后异步返回一个 Restauran 对象数组的类型方法来实现一个 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 拥有一个 String 类型的 name,一个二维坐标表示的 location 和包含嵌套 Meal 枚举值的 meals Set。
下例展示了在服务端响应中,一个餐馆是如何表示的。
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
}
编写可选 JSON 的初始化式
要将 JSON 表述转换为 Restaurant 对象,可以编写一个带有获取 JSON 数据并转化为属性的 Any 参数的初始化式。
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
}
}
如果 app 需要和一个或多个 web 服务器通信,并且服务器返回的不是单一、连续的模型对象表述,可以考虑实现几个初始化式,处理每个可能的表述。
在上例中,通过可选绑定和 as? 类型强制转换符,JSON 字典的每个值都被提取为常量了。对 name 属性来说,按原样赋值给提取的 name 值。对 coordinate 属性来说,赋值前将提取的 latitude、longitude 值结合为元组。对 meals 属性来说,迭代提取的字符串值以构造 Meal 枚举值的 Set。
编写代有错误处理的 JSON 初始化式
之前的例子实现了一个若反序列化失败则会返回 nil 的可选初始化式。也可以定义遵守 Error 协议的类型,实现一个每当反序列化失败时都会抛出相应类型 error 的初始化式。
enum SerializationError: Error {
case missing(String)
case invalid(String, Any)
}
extension Restaurant {
init(json: [String: Any]) throws {
// Extract name
guard let name = json["name"] as? String else {
throw SerializationError.missing("name")
}
// Extract and validate 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)
}
// Extract and validate 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)
}
// Initialize properties
self.name = name
self.coordinates = coordinates
self.meals = meals
}
}
这里的 Restaurant 类型声明了一个嵌套的 SerializationError 类型,为丢失或不合法的属性定义了带有关联值的枚举情况。此版本的 JSON 初始化式,并没有通过返回 nil 来标志失败,而是通过抛出 error 以说明具体的失败原因。这一版本也对 input 数据进行了验证,以保证 coordinates 是有效的地理坐标对,并且 JSON 中具体 meals 的每个名字都对应 Meal 枚举情况。
编写类型方法提取结果
Web 应用终端通常会在一个 JSON 中返回复杂的资源。例如,一个 /search 终端可能返回零或多个匹配请求查询参数的餐馆,包括其它元数据的表述:
{
"query": "sandwich",
"results_count": 12,
"page": 1,
"results": [
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
},
...
]
}
这时可以在 Restaurant 结构上创建一个类型方法,将 query 方法参数转化为相应的请求对象,并向 web 服务发送 HTTP 请求。这段代码同样可以处理响应、反序列化 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()
}
}
当用户在搜索条中输入文本时,view controller 可以调用这个方法,用匹配的餐馆数据填充表格视图:
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()
}
}
}
}
这样的关注点分离可以为 view controller 访问餐馆资源提供一致的接口,即使 web 服务的实现细节变化了也没关系。
关于反射的反思
为了不同系统间的通信,相同数据表述之间的转换对编写软件来说是一项乏味但必要的任务。
因为这些表述的结构可能非常相似,所以建立高层次的抽象以自动比对这些不同表述可能很有吸引力。例如,一种类型可能定义了 snake_case JSON 键和 camelCase 属性名间的映射,这样就可以根据 JSON,使用 Swift reflection API(比如 Mirror)自动初始化模型。
但是我们发现这种抽象在Swift语言特点下,日常使用起来意义不大,反而使得 debug 问题或处理边界情况更加困难了。在上述例子中,初始化式不仅从 JSON 提取、映射出值,同时可以初始化复杂的数据类型、执行特定域名的输入验证。为了实现这些任务,基于映射的方式成本更高。评估 app 可用策略时记住:少量重复的成本可能会比采用不当抽象的成本小得多。
专栏作者简介 ( )
古鲁伊 :立志做一名有格调的程序媛
打赏支持作者写出更多好文章,谢谢!
关注「 iOS大全 」
看更多精选 iOS 技术文章
↓↓↓