前言:
最近在寫關(guān)于網(wǎng)絡(luò)請求相關(guān)的代碼。簡單來說,我要做的事情是:
1、創(chuàng)建一個(gè) Swift Model 類
2、通過網(wǎng)絡(luò)請求 JSON 轉(zhuǎn) Model
3、把這個(gè) Model 存到沙盒里
4、把存起來的 Model 拿出來接著用
以上4步都是使用iOS原生的代碼做的事情,不使用三方框架。
在寫代碼的時(shí)候,也遇到了一些問題。所以,做個(gè)總結(jié)。
我再也不想寫這玩意了
Swift
代碼地址:https://github.com/gityuency/Autolayout
示例代碼類名 【SwiftCodingViewController】
運(yùn)行截圖


第一階段 認(rèn)識你的JSON
我們的JSON數(shù)據(jù)長這個(gè)樣子:
第一個(gè),最外層是個(gè)字典,里面包含了 字符串類型(String),整型(Int),浮點(diǎn)型(Float),布爾(Bool),并且包含了子對象,一個(gè)數(shù)組(Array),一個(gè)字典(Dictionary),這個(gè)JSON應(yīng)該具有些許的代表性了:
{
"name": "農(nóng)夫果園",
"location": "上海市 浦東新區(qū) 申迪北路 753號 上海迪士尼度假區(qū)",
"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
}
}
第二個(gè),最外層是個(gè)數(shù)組,數(shù)組里面也包含了 字符串類型(String),整型(Int),浮點(diǎn)型(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串你都得解出來,所以,在下面的的代碼里面,都有對應(yīng)的解法。
第二階段 寫你的Model
在上面的JSON串中,有很多不同的數(shù)據(jù)類型,所以,在寫Model的時(shí)候,也要注意類型,還有其他細(xì)節(jié)。關(guān)于這個(gè)Model,要注意的事情我都寫在了代碼注釋里面。
存對象,NSKeyedArchiver,需要繼承 NSCoding 協(xié)議
編碼解碼,JSONDecoder,需要繼承 Codable 協(xié)議
示例的 Model:
import Foundation
class HomePageModel: NSObject, NSCoding, Codable {
// 1號坑
// 如果這個(gè)字段 "name_wrong_example" 在后端返回過來的 json 串里沒有, 而這里定義類型 是 "String" 不是 "String?" 將會(huì)導(dǎo)致在 JSONDecoder 的時(shí)候字典轉(zhuǎn)模型失敗
// 同樣的道理, 不管定義的屬性類型是什么, 只要是在 json 串里沒有的, 不使用可選型,都會(huì)導(dǎo)致解析失敗,所以,為了安全起見,把這些屬性都定義為可選型吧
//var name_wrong_example: String = "初始值," //錯(cuò)誤
//var name_wrong_example: String? = "初始值" //正確
//var name_wrong_example: String? //正確
var name: String?
var number: Int?
// 2號坑
// 如果這個(gè)字段 "money" 在后端返回過來的 json 串里是浮點(diǎn)類型的, 有小數(shù)點(diǎn), 那么需要定義為 Float, 如果定義為 Int, 將會(huì)導(dǎo)致 JSONDecoder 的時(shí)候字典轉(zhuǎn)模型失敗
// 需要注意的問題是, 在json轉(zhuǎn)模型的時(shí)候, 這個(gè)字段的數(shù)值精度會(huì)丟失.
//var money: Int? = 998 //錯(cuò)誤,定義的類型和返回的json串里的類型不一致
//var money: Float? = 22.33 //正確 可以賦初始值
//var money: Float? //正確
var money: Float?
var open: Bool?
///"address"這個(gè)字段在 json 串里是沒有的, json 串里的 "location" 字段在這個(gè)模型里面也沒有定義, 這么做, 是為了查看 缺少字段, 寫錯(cuò)字段,會(huì)不會(huì)引起崩潰
///如果這里的 address 不使用 可選型, 寫成這樣: [ var address: String = "" ] 就炸了
var address: String?
// 對象里面包含了一個(gè)數(shù)組類型的值
var fruits: [FruitsInfo]?
// 對象里面還包含了一個(gè)對象
var owner: OwnerInfo?
override init() {
}
// NSCoding 協(xié)議里面的方法
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 協(xié)議里面的方法
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號坑
// 在這個(gè)方法里面, 如果解碼的方法調(diào)用不對,也是會(huì)造成失敗, 無法順利取出對象, 所有的屬性(Bool, String, Int ...), 在解碼的時(shí)候都要調(diào)用 decodeObject, 然后該轉(zhuǎn)類型的轉(zhuǎn)類型
//open = aDecoder.decodeBool(forKey: "open") //錯(cuò)誤, 不能因?yàn)槲抑浪莃ool類型就使用 "decodeBool", 因?yàn)檫@里定義的屬性都是可選型, 同樣, 也不能使用 "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 轉(zhuǎn) Model
這個(gè)地方就要使用JSON轉(zhuǎn)Model的類了,寫來寫去就那么幾句話,但是吧,不經(jīng)常寫,還是容易尷尬。我把這些代碼寫到了一個(gè)類里面。
代碼如下:
import Foundation
/// 字典轉(zhuǎn)模型工具類, 重復(fù)代碼不抽取
struct YXTransferToModel {
/// 字典轉(zhuǎn)模型
public static func toModelObject<T>(_ dictionary: Any?, to type: T.Type) -> T? where T: Decodable {
guard let dictionary = dictionary else {
print("? 傳入的數(shù)據(jù)解包失敗!")
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字典轉(zhuǎn)模型失敗!")
return nil
}
return model
}
/// 數(shù)組轉(zhuǎn)模型
public static func toModelArray<T>(_ array: Any?, to type: T.Type) -> [T]? where T: Decodable {
guard let array = array else {
print("? 傳入的數(shù)據(jù)解包失敗!")
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數(shù)組轉(zhuǎn)模型失敗!")
return nil
}
return arrayModel
}
}
第三階段 (插曲) Model 轉(zhuǎn) JSON,String
這都是經(jīng)常干的事情了,JSON 和 Model 互轉(zhuǎn),所以,也寫到一起。
代碼如下:
import Foundation
/*
來自網(wǎng)上的解釋
NSJSONReadingMutableContainers:返回可變?nèi)萜?,NSMutableDictionary或NSMutableArray。
NSJSONReadingMutableLeaves:返回的JSON對象中字符串的值為NSMutableString,目前在iOS 7上測試不好用,應(yīng)該是個(gè)bug,參見:
http://stackoverflow.com/questions/19345864/nsjsonreadingmutableleaves-option-is-not-working
NSJSONReadingAllowFragments:允許JSON字符串最外層既不是NSArray也不是NSDictionary,但必須是有效的JSON Fragment。例如使用這個(gè)選項(xiàng)可以解析 @“123” 這樣的字符串。參見:
http://stackoverflow.com/questions/16961025/nsjsonserialization-nsjsonreadingallowfragments-reading
*/
/// 字典轉(zhuǎn)模型工具類, 重復(fù)代碼不抽取
struct YXTransferToJson {
/// 模型轉(zhuǎn)字符串
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
}
/// 模型轉(zhuǎn)字典
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
}
/// 模型轉(zhuǎn)數(shù)組
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到數(shù)組失敗!")
return nil
}
return array
}
}
第四階段 把模型存到沙盒里
通過上面的 JSON 轉(zhuǎn) Model, 已經(jīng)拿到了 Model, 我們在斷網(wǎng)的情況下,也需要在頁面上顯示數(shù)據(jù),這就需要把 Model 存起來,先存?zhèn)€沙盒,使用 NSKeyedArchiver。
代碼如下:
import Foundation
/// 把 模型對象 或者 模型數(shù)組 存到 沙盒 里面, 重復(fù)代碼不抽取
struct YXSaverForSandBox {
static let KeyCacheModelName = "取一個(gè)好聽的名字"
static let KeyCacheArrayName = "你叫姬友最好聽"
private static let YXModelCache = "YXModelCache" //真機(jī)下面好像不能直接使用 Document 文件夾, 會(huì)引起崩潰, 所以自己創(chuàng)建一個(gè)文件夾
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("? 創(chuàng)建沙盒文件目錄失敗!")
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
}
/// 把 模型數(shù)組 存到 沙盒 里面
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("? 創(chuàng)建沙盒文件目錄失敗!")
return
}
let dataFullPath = "\(docPath)/\(YXModelCache)/\(key)"
NSKeyedArchiver.archiveRootObject(modelArray, toFile: dataFullPath)
}
/// 把 模型數(shù)組 從 沙盒 里取出來
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
}
/// 把模型存到沙盒中,使用日期區(qū)分,這樣會(huì)存很多的文件
static func save(model: NSCoding) {
let date = Date()
dateFormatter.dateFormat = "yyyy年 MM月 dd日 HH時(shí) 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("? 創(chuàng)建沙盒文件目錄失敗!")
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 里
有時(shí)候吧,為了圖省事,存到 UserDefaults 里這種事情也是干得出來的,所以,把這樣的方法也寫到一起。
代碼如下:
import Foundation
/// 把 模型對象 或者 模型數(shù)組 存到 UserDefaults 里面, 重復(fù)代碼不抽取
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轉(zhuǎn)模型失敗!")
return nil
}
return model
}
//MARK: - Array
/// 把 模型數(shù)組 存到 UserDefaults 里面
static func saveToUserDefaults(key: String, with modelArray: [NSCoding]) {
let data = NSKeyedArchiver.archivedData(withRootObject: modelArray)
UserDefaults.standard.set(data, forKey: key)
}
/// 把 模型數(shù)組 從 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轉(zhuǎn)模型失敗!")
return nil
}
return array
}
/// 清理數(shù)據(jù)
static func clearCacheModel(key: String) {
UserDefaults.standard.removeObject(forKey: key)
}
}
第五階段 測試代碼
到這里代碼都寫得差不多了,現(xiàn)在創(chuàng)建一個(gè) ViewController,來測試并使用那些代碼。
這里我使用了 https://github.com/Alamofire/Alamofire 來發(fā)網(wǎng)絡(luò)請求,
還用了花瓶 https://www.charlesproxy.com/
的 “Map Local” 功能來Mock數(shù)據(jù)。只是為了,看起來,就像真的一樣。
ViewController 代碼如下:
import UIKit
import Alamofire
class SwiftCodingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
/// 字典轉(zhuǎn)模型
@IBAction func actionDicToModel(_ sender: UIButton) {
let urlString = "http://www.yuency.com/yuencyDictionary.json"
Alamofire.request(urlString).responseJSON { (json) in
switch json.result {
case .success:
// 字典轉(zhuǎn)模型
let modelObject = YXTransferToModel.toModelObject(json.result.value, to: HomePageModel.self)
//模型轉(zhuǎn) 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!)
// 按照日期把數(shù)據(jù)存到沙盒(會(huì)存很多這樣的文件)
YXSaverForSandBox.save(model: modelObject!)
case .failure(let error):
print("網(wǎng)絡(luò)請求失敗 \(error)")
}
}
}
/// 數(shù)組轉(zhuǎn)模型
@IBAction func actionArrToModel(_ sender: UIButton) {
let urlString = "http://www.yuency.com/yuencyArray.json"
Alamofire.request(urlString).responseJSON { (json) in
switch json.result {
case .success:
// 數(shù)組轉(zhuǎn)模型
let modelArray = YXTransferToModel.toModelArray(json.result.value, to: FruitsInfo.self)
//模型數(shù)組轉(zhuǎn) json / Array
print("--->\n \(YXTransferToJson.model(toString: modelArray) ?? "失敗")")
print("--->\n \(YXTransferToJson.model(toArray: modelArray) ?? ["失敗"])")
// 把模型數(shù)組 存到 userdefault 里
YXSaverForUserDefaults.saveToUserDefaults(key: YXSaverForUserDefaults.KeyFruitsArray, with: modelArray!)
// 把模型數(shù)組 存到沙盒里
YXSaverForSandBox.saveToSandBox(key: YXSaverForSandBox.KeyCacheArrayName, with: modelArray!)
case .failure(let error):
print("網(wǎng)絡(luò)請求失敗 \(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 ?? "沒有解出來")
//從沙盒里取出模型數(shù)組
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()
}
}
結(jié)語:
現(xiàn)在應(yīng)該說是前年了,好像是8月17號的樣子。天還很熱,寫代碼的時(shí)候,靠著窗子,對著大屏幕,聽著這首鬼畜 https://www.bilibili.com/video/av3816897?from=search&seid=3916296380692081353 感覺還挺亢奮。還有一首他的電音之王,貌似搜不到了。有些東西啊,再聽的時(shí)候,就覺得,年代久遠(yuǎn)了。但愿從沒聽過