iOS Swift 原生 字典数组转模型 JSONDecoder 对象存储 NSKeyedArchiver

前言:

最近在写关于网络请求相关的代码。简单来说,我要做的事情是:

1、创建一个 Swift Model 类
2、通过网络请求 JSON 转 Model
3、把这个 Model 存到沙盒里
4、把存起来的 Model 拿出来接着用

以上4步都是使用iOS原生的代码做的事情,不使用三方框架。
在写代码的时候,也遇到了一些问题。所以,做个总结。
我再也不想写这玩意了

Swift
代码地址:https://github.com/gityuency/Autolayout
示例代码类名 【SwiftCodingViewController】

运行截图

运行截图1.png
运行截图2.png

第一阶段 认识你的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(_ 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(_ 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(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(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(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(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(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(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(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 感觉还挺亢奋。还有一首他的电音之王,貌似搜不到了。有些东西啊,再听的时候,就觉得,年代久远了。但愿从没听过

你可能感兴趣的:(iOS Swift 原生 字典数组转模型 JSONDecoder 对象存储 NSKeyedArchiver)