分类 标签 存档 黑客派 订阅 搜索

Swift 中使用 JSON(译)

202 浏览

如果你的 APP 跟后台的 web 应用做交互, 服务器返回的信息一般都是 JSON 格式的. 你可以用Foundation框架的JSONSerialazation类把 JSON 转换为 Swift 数据类型, 例如:Dictionary,Array,String,NumberBool. 然而你不能确定你的 APP 接收 JSON 数据的结构或者具体的值, 这就使得能够正确的反序列化一个模型对象成为一个挑战. 这篇文章介绍了一些你可以在你的 APP 中使用 JSON 的方法.

从 JSON 中提取值

JSONSerialization类的jsonObject(with: options: )方法返回一个Any类型的值并且如果数据不能被解析会抛出一个异常.

import Foundation
let data: Data //例如从一个网络请求中接收的数据
let json = try? JSONSerialization.jsonObject(with: data, options: [])

尽管服务器返回的有效的 JSON 数据可能只包含一个简单值, 一个从 web 应用返回的响应一般编码成一个对象或者数组来当做顶层的对象. 你可以在if或者guard语句中使用可选绑定和as?类型转换操作来提取一个已知类型的值作为常数. 为了从 JSON 对象类型得到一个Dictionary值, 有条件的把它 (上面的常量) 转换为[String: Any]类型. 为了从 JSON 对象类型得到一个Array值, 有条件的把它转换为[Any]类型 (或者有特定元素类型的数组, 像[String]). 你可以用 key 提取字典中的值或者用类型转换可选绑定下表选择器, 枚举匹配模式提取数组中的值.

//JSON根对象示例:
/*
 {
    "someKey": 42.0,
    "anotherKey": {
        "someNestedKey": true
    }
 }
 */

if let dictionary = jsonWithObjectRoot as? [String: Any] {
    if let number = dictionary["someKey"] as? Double {
        //访问字典中个别的值
    }

    for (key, value) in dictionary {
        //访问字典中所有的key 和 value
    }

    if let nestedDictionary = dictionary["anotherKey"] as? [String: Any] {
        //通过key访问字典中的nested字典
    }
}

//JSON根数组示例
/*
 [
    "hello", 3, true
 ]
 */
if let array = jsonWithObjectRoot as? [Any] {

    if let firstObject = array.first {
        //访问第一个元素
    }

    for obj in array {
        //访问数组中所有数据
    }

    for case let string as String in array {
        //访问字符串类型的数据
    }
}

Swift 的内置语言功能 Foundation APIs 能很容易并且安全提取 JSON 数据 - 无需外部库或者框架

用从 JSON 提取的值常见模型对象

自从大多数 Swift 应用遵循 MVC 设计模式, 他转换成 JSON 数据是特定于一个模型定义你的应用程序的与对象往往是有益的.

例如, 当为本地餐馆写一个提供搜索结果的应用时, 你可能继承一个有接受 JSON 对象的狗在其和一个发送 HTTP 请求到服务器/search端口然后异步返回一个Restaurant数组的类方法.

考虑下面的Restaurant模型:

import Foundation

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

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

一个Restaurant模型有一个String类型的name,location表示一个坐标对, 和一个Set类型的meals, 包含一系列嵌套枚举Meal的值.

这里有一个例子来, 一个单一餐厅可能在服务器的响应:

 {
    "name": "Caffe Macs",
    "coordinates": {
        "lat": 37.33.576,
        "lng": -122.029739
    },
    "meals": ["breakfast", "lunch", "dinner"]
 }

写一个可选 JSON 构造器

extension Restaurant {
    init?(json: [String: Any]) {
        guard let name = json["name"] as? String,
        let coordinatesJSON = json["coordinates"] as? [String: Any],
        let latitude = coordinatesJSON["lat"] as? Double,
        let longitude = coordinatesJSON["lng"] as? Double,
        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.location = (latitude, longitude)
        self.meals = meals
    }
}

如果你的 APP 跟一个或者多个没有返回简单, 恰表现型模型对象的 web 服务作交互, 考虑继承一些初始化构造器来处理每一个可能的表现.

在上面的例子中, 每一个值都能通过 JSON 字典用可选绑定和as?类型转换操作当做常数被提取出来. 例如name属性, 被提取出来的name值被简单的分配. 例如coordinate属性, 被提取出来的latitudelongitude值在分配之前被组合成一个元组. 例如meals属性, 被提取出来的字符串类型值被迭代成一个Meal枚举值的Set类型的常量.

写一个带有错误处理的 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")
        }

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

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

        //提取验证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.location = coordinates
        self.meals = meals
    }
}

在这里Restaurant声明了一个嵌套的SerializationError类型, 这个嵌套类型为缺失或者是无效属性定义了相关的枚举值. 在抛异常版的 JSON 构造器中, 用抛异常的方式来说明初始化一个值失败, 而不是通过返回nil来说明. 这个版本也对输入数据执行验证以确保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查询参数转换为对应的请求对象, 并且发送 HTTP 请求到 web 服务器. 这段代码也负责处理响应, 反序列化 JSON 数据, 并用从results数组中提取出来的字典创建Restaurant对象, 然后在一个回调中异步的返回它们.

extension Restaurant {
    private let urlComponents: URLComponents // web服务的基本网址组件
    private let session: URLSession // 用于与web服务做交互的共享对话

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

当用户在搜索框中输入内容匹配到相应的餐厅并填充到 table view 上时视图控制器可以调用这个方法.

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 服务要发生修改.

反思反射

在开发软件中, 为了在两个不同的系统之间做交互而转换相同数据的不同表现形式是繁琐并且有必要的.

因为这些表现形式的结构可能很相似, 它可能诱使你创建更高级别的抽象来自动映射这些不同的表示. 例如, 一个类型可能为了从 JSON 中自动初始化一个 model 定义一个snake_caseJSON 的键和camelCase属性映射, 用 Swift 反射 APIs, 例如Mirror

然而, 我们发现这些类型的抽象跟传统的使用 Swift 语言功能相比并不提供明显的好处, 换来的却是难以定位问题或者处理边界情况. 在上面的例子中, 构造器不仅从 JSON 中提取和映射值, 而且还初始化复杂的数据类型并且执行了特定领域的输入验证. 一个基于反射的方法不得不写的很长来完成这些任务. 在评估你自己应用程序的可用策略时记住这一个想法. 少量重复的成本可能明显低于采用不正确的抽象.

原文链接: https://developer.apple.com/swift/blog/?id=37

--EOF--

评论  
留下你的脚步
推荐阅读