前言:
最近在写关于网络请求相关的代码。简单来说,我要做的事情是:
1、创建一个 Swift Model 类
2、通过网络请求 JSON 转 Model
3、把这个 Model 存到沙盒里
4、把存起来的 Model 拿出来接着用
以上4步都是使用iOS原生的代码做的事情,不使用三方框架。
在写代码的时候,也遇到了一些问题。所以,做个总结。
我再也不想写这玩意了
Swift
代码地址:https://github.com/gityuency/Autolayout
示例代码类名 【SwiftCodingViewController】
运行截图
第一阶段 认识你的JSON
我们的JSON数据长这个样子:
第一个,最外层是个字典,里面包含了 字符串类型(String),整型(Int),浮点型(Float),布尔(Bool),并且包含了子对象,一个数组(Array),一个字典(Dictionary),这个JSON应该具有些许的代表性了:
{
"name": "农夫果园",
"location": "上海市 浦东新区 申迪北路 753号 上海迪士尼度假区",
"number": 10001,
"money": 998.12,
"open": true,
"fruits": [
{
"name": "火龙果",
"count": 2000,
"price": 56.23,
"onsale": true
},
{
"name": "山竹",
"count": 555,
"price": 17.22,
"onsale": false
}
],
"owner": {
"name": "姬友大人",
"age": 30
}
}
第二个,最外层是个数组,数组里面也包含了 字符串类型(String),整型(Int),浮点型(Float),布尔(Bool):
[
{
"name": "苹果🍎",
"count": 8855,
"price": 6.23,
"onsale": false
},
{
"name": "菠萝🍍",
"count": 555,
"price": 55.22,
"onsale": false
},
{
"name": "樱桃🍒",
"count": 2567,
"price": 100.5,
"onsale": true
}
]
这两种类型的JSON串你都得解出来,所以,在下面的的代码里面,都有对应的解法。
第二阶段 写你的Model
在上面的JSON串中,有很多不同的数据类型,所以,在写Model的时候,也要注意类型,还有其他细节。关于这个Model,要注意的事情我都写在了代码注释里面。
存对象,NSKeyedArchiver,需要继承 NSCoding 协议
编码解码,JSONDecoder,需要继承 Codable 协议
示例的 Model:
import Foundation
class HomePageModel: NSObject, NSCoding, Codable {
// 1号坑
// 如果这个字段 "name_wrong_example" 在后端返回过来的 json 串里没有, 而这里定义类型 是 "String" 不是 "String?" 将会导致在 JSONDecoder 的时候字典转模型失败
// 同样的道理, 不管定义的属性类型是什么, 只要是在 json 串里没有的, 不使用可选型,都会导致解析失败,所以,为了安全起见,把这些属性都定义为可选型吧
//var name_wrong_example: String = "初始值," //错误
//var name_wrong_example: String? = "初始值" //正确
//var name_wrong_example: String? //正确
var name: String?
var number: Int?
// 2号坑
// 如果这个字段 "money" 在后端返回过来的 json 串里是浮点类型的, 有小数点, 那么需要定义为 Float, 如果定义为 Int, 将会导致 JSONDecoder 的时候字典转模型失败
// 需要注意的问题是, 在json转模型的时候, 这个字段的数值精度会丢失.
//var money: Int? = 998 //错误,定义的类型和返回的json串里的类型不一致
//var money: Float? = 22.33 //正确 可以赋初始值
//var money: Float? //正确
var money: Float?
var open: Bool?
///"address"这个字段在 json 串里是没有的, json 串里的 "location" 字段在这个模型里面也没有定义, 这么做, 是为了查看 缺少字段, 写错字段,会不会引起崩溃
///如果这里的 address 不使用 可选型, 写成这样: [ var address: String = "" ] 就炸了
var address: String?
// 对象里面包含了一个数组类型的值
var fruits: [FruitsInfo]?
// 对象里面还包含了一个对象
var owner: OwnerInfo?
override init() {
}
// NSCoding 协议里面的方法
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(number, forKey: "number")
aCoder.encode(money, forKey: "money")
aCoder.encode(open, forKey: "open")
aCoder.encode(fruits, forKey: "fruits")
aCoder.encode(owner, forKey: "owner")
}
// NSCoding 协议里面的方法
required init?(coder aDecoder: NSCoder) {
super.init()
name = (aDecoder.decodeObject(forKey: "name") as? String) ?? ""
number = aDecoder.decodeObject(forKey: "number") as? Int
money = aDecoder.decodeObject(forKey: "money") as? Float
// 3号坑
// 在这个方法里面, 如果解码的方法调用不对,也是会造成失败, 无法顺利取出对象, 所有的属性(Bool, String, Int ...), 在解码的时候都要调用 decodeObject, 然后该转类型的转类型
//open = aDecoder.decodeBool(forKey: "open") //错误, 不能因为我知道它是bool类型就使用 "decodeBool", 因为这里定义的属性都是可选型, 同样, 也不能使用 "decodeInteger" 这样明确解码类型的方法去解码其他可选型的属性.
open = aDecoder.decodeObject(forKey: "open") as? Bool //正确
fruits = aDecoder.decodeObject(forKey: "fruits") as? [FruitsInfo]
owner = aDecoder.decodeObject(forKey: "owner") as? OwnerInfo
}
}
/// 二级模型 水果摊
class FruitsInfo: NSObject, NSCoding, Codable {
var name: String?
var count: Int?
var price: Float?
var onsale: Bool?
override init() {
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.count, forKey: "count")
aCoder.encode(self.price, forKey: "price")
aCoder.encode(self.onsale, forKey: "onsale")
}
required init?(coder aDecoder: NSCoder) {
super.init()
name = (aDecoder.decodeObject(forKey: "name") as? String) ?? ""
count = aDecoder.decodeObject(forKey: "count") as? Int
price = aDecoder.decodeObject(forKey: "price") as? Float
onsale = aDecoder.decodeObject(forKey: "onsale") as? Bool
}
}
/// 二级模型 商店老板
class OwnerInfo: NSObject, NSCoding, Codable {
var name: String?
var age: Int?
override init() {
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
}
required init?(coder aDecoder: NSCoder) {
super.init()
name = (aDecoder.decodeObject(forKey: "name") as? String) ?? ""
age = aDecoder.decodeObject(forKey: "age") as? Int
}
}
extension HomePageModel {
override var description: String {
return """
\(String(describing: name))
\(String(describing: number))
\(String(describing: money))
\(String(describing: open))
\(String(describing: address))
\(String(describing: fruits))
\(String(describing: owner))
"""
}
}
extension FruitsInfo {
override var description: String {
return """
\(String(describing: name))
\(String(describing: count))
\(String(describing: price))
\(String(describing: onsale))
"""
}
}
extension OwnerInfo {
override var description: String {
return """
\(String(describing: name))
\(String(describing: age))
"""
}
}
第三阶段 JSON 转 Model
这个地方就要使用JSON转Model的类了,写来写去就那么几句话,但是吧,不经常写,还是容易尴尬。我把这些代码写到了一个类里面。
代码如下:
import Foundation
/// 字典转模型工具类, 重复代码不抽取
struct YXTransferToModel {
/// 字典转模型
public static func toModelObject<T>(_ dictionary: Any?, to type: T.Type) -> T? where T: Decodable {
guard let dictionary = dictionary else {
print("❌ 传入的数据解包失败!")
return nil
}
if !JSONSerialization.isValidJSONObject(dictionary) {
print("❌ 不是合法的json对象!")
return nil
}
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
print("❌ JSONSerialization序列化失败!")
return nil
}
guard let model = try? JSONDecoder().decode(type, from: data) else {
print("❌ JSONDecoder字典转模型失败!")
return nil
}
return model
}
/// 数组转模型
public static func toModelArray<T>(_ array: Any?, to type: T.Type) -> [T]? where T: Decodable {
guard let array = array else {
print("❌ 传入的数据解包失败!")
return nil
}
if !JSONSerialization.isValidJSONObject(array) {
print("❌ 不是合法的json对象!")
return nil
}
guard let data = try? JSONSerialization.data(withJSONObject: array, options: []) else {
print("❌ JSONSerialization序列化失败!")
return nil
}
guard let arrayModel = try? JSONDecoder().decode([T].self, from: data) else {
print("❌ JSONDecoder数组转模型失败!")
return nil
}
return arrayModel
}
}
第三阶段 (插曲) Model 转 JSON,String
这都是经常干的事情了,JSON 和 Model 互转,所以,也写到一起。
代码如下:
import Foundation
/*
来自网上的解释
NSJSONReadingMutableContainers:返回可变容器,NSMutableDictionary或NSMutableArray。
NSJSONReadingMutableLeaves:返回的JSON对象中字符串的值为NSMutableString,目前在iOS 7上测试不好用,应该是个bug,参见:
http://stackoverflow.com/questions/19345864/nsjsonreadingmutableleaves-option-is-not-working
NSJSONReadingAllowFragments:允许JSON字符串最外层既不是NSArray也不是NSDictionary,但必须是有效的JSON Fragment。例如使用这个选项可以解析 @“123” 这样的字符串。参见:
http://stackoverflow.com/questions/16961025/nsjsonserialization-nsjsonreadingallowfragments-reading
*/
/// 字典转模型工具类, 重复代码不抽取
struct YXTransferToJson {
/// 模型转字符串
public static func model<T>(toString model: T) -> String? where T: Encodable {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
guard let data = try? jsonEncoder.encode(model) else {
print("❌ jsonEncoder解码失败!")
return nil
}
guard let jsonString = String(data: data, encoding: .utf8) else {
print("❌ data到字符串失败!")
return nil
}
return jsonString
}
/// 模型转字典
public static func model<T>(toDictionary model: T) -> [String: Any]? where T: Encodable {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
guard let data = try? jsonEncoder.encode(model) else {
print("❌ jsonEncoder解码失败!")
return nil
}
guard let dictionary = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] else {
print("❌ data到字典失败!")
return nil
}
return dictionary
}
/// 模型转数组
public static func model<T>(toArray model: T) -> [Any]? where T: Encodable {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
guard let data = try? jsonEncoder.encode(model) else {
print("❌ jsonEncoder解码失败!")
return nil
}
guard let array = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [Any] else {
print("❌ data到数组失败!")
return nil
}
return array
}
}
第四阶段 把模型存到沙盒里
通过上面的 JSON 转 Model, 已经拿到了 Model, 我们在断网的情况下,也需要在页面上显示数据,这就需要把 Model 存起来,先存个沙盒,使用 NSKeyedArchiver。
代码如下:
import Foundation
/// 把 模型对象 或者 模型数组 存到 沙盒 里面, 重复代码不抽取
struct YXSaverForSandBox {
static let KeyCacheModelName = "取一个好听的名字"
static let KeyCacheArrayName = "你叫姬友最好听"
private static let YXModelCache = "YXModelCache" //真机下面好像不能直接使用 Document 文件夹, 会引起崩溃, 所以自己创建一个文件夹
private static let dateFormatter = DateFormatter()
/// 把 模型对象 存到 沙盒 里面
static func saveToSandBox(key: String, with modelObject: NSCoding) {
guard let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
print("❌ 获取沙盒目录失败!")
return
}
let userDirPath = URL(fileURLWithPath: docPath).appendingPathComponent(YXModelCache)
guard (try? FileManager.default.createDirectory(at: userDirPath, withIntermediateDirectories: true, attributes: [:])) != nil else {
print("❌ 创建沙盒文件目录失败!")
return
}
let dataFullPath = "\(docPath)/\(YXModelCache)/\(key)"
NSKeyedArchiver.archiveRootObject(modelObject, toFile: dataFullPath)
}
/// 把 模型对象 从 沙盒 里取出来
static func fetchFromSandBox<T>(key: String, asObject type: T.Type) -> T? where T: NSCoding {
guard let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
print("❌ 获取沙盒目录失败!")
return nil
}
let dataFullPath = "\(docPath)/\(YXModelCache)/\(key)"
guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: dataFullPath) as? T else {
print("❌ unarchiveObject 失败!")
return nil
}
return data
}
/// 把 模型数组 存到 沙盒 里面
static func saveToSandBox(key: String, with modelArray: [NSCoding]) {
guard let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
print("❌ 获取沙盒目录失败!")
return
}
let userDirPath = URL(fileURLWithPath: docPath).appendingPathComponent(YXModelCache)
guard (try? FileManager.default.createDirectory(at: userDirPath, withIntermediateDirectories: true, attributes: [:])) != nil else {
print("❌ 创建沙盒文件目录失败!")
return
}
let dataFullPath = "\(docPath)/\(YXModelCache)/\(key)"
NSKeyedArchiver.archiveRootObject(modelArray, toFile: dataFullPath)
}
/// 把 模型数组 从 沙盒 里取出来
static func fetchFromSandBox<T>(key: String, asArray type: T.Type) -> [T]? where T: NSCoding {
guard let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
print("❌ 获取沙盒目录失败!")
return nil
}
let dataFullPath = "\(docPath)/\(YXModelCache)/\(key)"
guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: dataFullPath) as? [T] else {
print("❌ unarchiveObject 失败!")
return nil
}
return data
}
/// 把模型存到沙盒中,使用日期区分,这样会存很多的文件
static func save(model: NSCoding) {
let date = Date()
dateFormatter.dateFormat = "yyyy年 MM月 dd日 HH时 mm分 ss秒 SSS毫秒"
let strDate = dateFormatter.string(from: date)
guard let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
print("❌ 获取沙盒目录失败!")
return
}
let userDirPath = URL(fileURLWithPath: docPath).appendingPathComponent(YXModelCache)
guard (try? FileManager.default.createDirectory(at: userDirPath, withIntermediateDirectories: true, attributes: [:])) != nil else {
print("❌ 创建沙盒文件目录失败!")
return
}
let dataFullPath = "\(docPath)/\(YXModelCache)/\(strDate)"
NSKeyedArchiver.archiveRootObject(model, toFile: dataFullPath)
}
/// 删掉沙盒里的文件
static func removeAll() {
guard let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
print("❌ 获取沙盒目录失败!")
return
}
let dirPath = "\(docPath)/\(YXModelCache)"
guard ((try? FileManager.default.removeItem(atPath: dirPath)) != nil) else {
print("❌ 删除文件夹失败!")
return
}
}
}
第四阶段(插曲) 把模型存到 UserDefaults 里
有时候吧,为了图省事,存到 UserDefaults 里这种事情也是干得出来的,所以,把这样的方法也写到一起。
代码如下:
import Foundation
/// 把 模型对象 或者 模型数组 存到 UserDefaults 里面, 重复代码不抽取
struct YXSaverForUserDefaults {
//MARK: - Key
static let KeyHomePageModel = "KeyHomePageModel"
static let KeyFruitsArray = "KeyFruitsArray"
//MARK: - Model
/// 把 模型对象 存到 UserDefaults 里面
static func saveToUserDefaults(key: String, with modelObject: NSCoding) {
let data = NSKeyedArchiver.archivedData(withRootObject: modelObject)
UserDefaults.standard.set(data, forKey: key)
}
/// 把 模型对象 从 UserDefaults 里取出来
static func fetchFromUserDefaults<T>(key: String, asObject type: T.Type) -> T? where T: NSCoding {
guard let data = UserDefaults.standard.value(forKey: key) as? Data else {
print("❌ 从UserDefault里解析data失败!")
return nil
}
guard let model = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? T else {
print("❌ data转模型失败!")
return nil
}
return model
}
//MARK: - Array
/// 把 模型数组 存到 UserDefaults 里面
static func saveToUserDefaults(key: String, with modelArray: [NSCoding]) {
let data = NSKeyedArchiver.archivedData(withRootObject: modelArray)
UserDefaults.standard.set(data, forKey: key)
}
/// 把 模型数组 从 UserDefaults 里取出来
static func fetchFromUserDefaults<T>(key: String, asArray type: T.Type) -> [T]? where T: NSCoding {
guard let data = UserDefaults.standard.value(forKey: key) as? Data else {
print("❌ 从UserDefault里解析data失败!")
return nil
}
guard let array = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [T] else {
print("❌ data转模型失败!")
return nil
}
return array
}
/// 清理数据
static func clearCacheModel(key: String) {
UserDefaults.standard.removeObject(forKey: key)
}
}
第五阶段 测试代码
到这里代码都写得差不多了,现在创建一个 ViewController,来测试并使用那些代码。
这里我使用了 https://github.com/Alamofire/Alamofire 来发网络请求,
还用了花瓶 https://www.charlesproxy.com/
的 “Map Local” 功能来Mock数据。只是为了,看起来,就像真的一样。
ViewController 代码如下:
import UIKit
import Alamofire
class SwiftCodingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
/// 字典转模型
@IBAction func actionDicToModel(_ sender: UIButton) {
let urlString = "http://www.yuency.com/yuencyDictionary.json"
Alamofire.request(urlString).responseJSON { (json) in
switch json.result {
case .success:
// 字典转模型
let modelObject = YXTransferToModel.toModelObject(json.result.value, to: HomePageModel.self)
//模型转 json / 字典
print("--->\n \(YXTransferToJson.model(toString: modelObject) ?? "失败")")
print("--->\n \(YXTransferToJson.model(toDictionary: modelObject) ?? ["失败":"失败"])")
// 把模型 存到 userdefault 里
YXSaverForUserDefaults.saveToUserDefaults(key: YXSaverForUserDefaults.KeyHomePageModel, with: modelObject!)
// 把模型存到沙盒里
YXSaverForSandBox.saveToSandBox(key: YXSaverForSandBox.KeyCacheModelName, with: modelObject!)
// 按照日期把数据存到沙盒(会存很多这样的文件)
YXSaverForSandBox.save(model: modelObject!)
case .failure(let error):
print("网络请求失败 \(error)")
}
}
}
/// 数组转模型
@IBAction func actionArrToModel(_ sender: UIButton) {
let urlString = "http://www.yuency.com/yuencyArray.json"
Alamofire.request(urlString).responseJSON { (json) in
switch json.result {
case .success:
// 数组转模型
let modelArray = YXTransferToModel.toModelArray(json.result.value, to: FruitsInfo.self)
//模型数组转 json / Array
print("--->\n \(YXTransferToJson.model(toString: modelArray) ?? "失败")")
print("--->\n \(YXTransferToJson.model(toArray: modelArray) ?? ["失败"])")
// 把模型数组 存到 userdefault 里
YXSaverForUserDefaults.saveToUserDefaults(key: YXSaverForUserDefaults.KeyFruitsArray, with: modelArray!)
// 把模型数组 存到沙盒里
YXSaverForSandBox.saveToSandBox(key: YXSaverForSandBox.KeyCacheArrayName, with: modelArray!)
case .failure(let error):
print("网络请求失败 \(error)")
}
}
}
@IBAction func actionFetchDataFromUserDefault(_ sender: UIButton) {
// 从UserDefaults取出模型
let modelObject = YXSaverForUserDefaults.fetchFromUserDefaults(key: YXSaverForUserDefaults.KeyHomePageModel, asObject: HomePageModel.self)
print(modelObject ?? "没有解出来")
// 从UserDefaults取出模型
let modelArray = YXSaverForUserDefaults.fetchFromUserDefaults(key: YXSaverForUserDefaults.KeyFruitsArray, asArray: FruitsInfo.self)
print(modelArray ?? "没有解出来")
}
@IBAction func actionFetchDataFromSandBox(_ sender: UIButton) {
//从沙盒里取出模型
let modelObjectFromSanBox = YXSaverForSandBox.fetchFromSandBox(key: YXSaverForSandBox.KeyCacheModelName, asObject: HomePageModel.self)
print(modelObjectFromSanBox ?? "没有解出来")
//从沙盒里取出模型数组
let modelArrayFromSanBox = YXSaverForSandBox.fetchFromSandBox(key: YXSaverForSandBox.KeyCacheArrayName, asArray: FruitsInfo.self)
print(modelArrayFromSanBox ?? "没有解出来")
}
@IBAction func actionClearAll(_ sender: UIButton) {
YXSaverForUserDefaults.clearCacheModel(key: YXSaverForUserDefaults.KeyHomePageModel)
YXSaverForUserDefaults.clearCacheModel(key: YXSaverForUserDefaults.KeyFruitsArray)
YXSaverForSandBox.removeAll()
}
}
结语:
现在应该说是前年了,好像是8月17号的样子。天还很热,写代码的时候,靠着窗子,对着大屏幕,听着这首鬼畜 https://www.bilibili.com/video/av3816897?from=search&seid=3916296380692081353 感觉还挺亢奋。还有一首他的电音之王,貌似搜不到了。有些东西啊,再听的时候,就觉得,年代久远了。但愿从没听过