Swift中使用JSON(译)

e5fb1e4f7b9a45690bf744ba71d4a7b0_b.jpeg

如果你的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
本人博客: https://jingxuetao.com

--EOF--

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容