前面文章中 UserDefaults 的基本用法 中對(duì)UserDefaults 進(jìn)行了簡(jiǎn)單的介紹,它可以將一些簡(jiǎn)單的數(shù)據(jù)類(lèi)型存儲(chǔ)在本地,需要使用的時(shí)候再去讀取。
如果對(duì)于復(fù)雜對(duì)象的存儲(chǔ)則需要將其進(jìn)行序列化,將對(duì)象轉(zhuǎn)化為 NSData(Swift Data)類(lèi)型之后再進(jìn)行操作,比如,將其存在本地的某個(gè)文件(eg.people.plist, people.txt等)中。
有2種序列化的方式:
-
NSCoding: 老的Cocoa方式,OC的方式 -
Codable: 新的swift方式
NSCoding
這個(gè)協(xié)議在Cocoa的Foundation框架中定義,內(nèi)置的大多數(shù)Cocoa類(lèi)都采用了NSCoding協(xié)議,比如 UIColor 等。
采用了這個(gè)協(xié)議的對(duì)象可以轉(zhuǎn)換為 NSData 類(lèi)型,然后再轉(zhuǎn)換回來(lái)。使用 NSKeyedArchiver 和 NSKeyedUnarchiver 分別進(jìn)行歸檔和解檔。
采用這個(gè)協(xié)議的對(duì)象需要實(shí)現(xiàn) encode(with:) 進(jìn)行歸檔,以及 init(coder:) 進(jìn)行解檔。
比如,自定義的 Person 類(lèi),有2點(diǎn)值得說(shuō)明的, 來(lái)源NSCoding - hackingwithswift:
- 為什么使用class,而不是struct? 因?yàn)镹SCoding需要使用對(duì)象,或者在字符串,數(shù)組,字典的情況下,使用可以與對(duì)象互換的結(jié)構(gòu),如果把Person當(dāng)做一個(gè)struct,我們不能在NSCoding中使用它
-
為什么要繼承
NSObject? 因?yàn)槭褂肗SCoding,必須使用NSObject,否則應(yīng)用會(huì)崩潰
class Person: NSObject, NSCoding {
var firstName: String
var lastName: String
var age: Int
// 如果定義一個(gè)實(shí)例Person,打印結(jié)果將是這里定義的描述字符串
override var descirption: String {
return "\(self.firstName) \(self.lastName) \(age)"
}
init(firstName: String, lastName: String, age: Int) {
self.firstName = firstName
self.lastName = lastName
self.age = age
}
// 實(shí)現(xiàn)NSCoding 協(xié)議中的方法
func encode(with aCoder: NSCoder) {
// 如果Person 還有一個(gè)父類(lèi),假設(shè)Human也采用了NSCoding協(xié)議
// 則必須先調(diào)用父類(lèi)的 super
// 這里不需要
aCoder.encode(self.firstName, forKey: "first")
aCoder.encode(self.lastName, forKey: "last")
aCoder.encode(self.age, forKey: "age")
}
required init?(coder aDecoder: NSCoder) {
// 同上,如果存在父類(lèi)采用NSCoding協(xié)議,則也需要先調(diào)用父類(lèi)的構(gòu)造器
// 注意這里返回的是 NSString 類(lèi)型
self.firstName = aDecoder.decodeObject(of: NSString.self, forKey: "first")! as String
self.lastName = aDecoder.decodeObject(of: NSString.self, forKey: "last")! as String
// 對(duì)于Int類(lèi)型
self.age = aDecoder.decodeInteger(forKey: "age")
}
}
在 iOS12中,蘋(píng)果推薦使用 NSSecureCoding 協(xié)議,這個(gè)協(xié)議在NSCoding的基礎(chǔ)上,還需要實(shí)現(xiàn)一個(gè)靜態(tài)的 static var supportsSecureCoding: Bool {return true} 屬性。
class Person: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { return true } // 需要添加這個(gè)靜態(tài)屬性
var firstName: String
var lastName: String
var age: Int
// 如果定義一個(gè)實(shí)例Person,打印結(jié)果將是這里定義的描述字符串
override var descirption: String {
return "\(self.firstName) \(self.lastName) \(age)"
}
init(firstName: String, lastName: String, age: Int) {
self.firstName = firstName
self.lastName = lastName
self.age = age
}
// 實(shí)現(xiàn)NSCoding 協(xié)議中的方法
func encode(with aCoder: NSCoder) {
// 如果Person 還有一個(gè)父類(lèi),假設(shè)Human也采用了NSCoding協(xié)議
// 則必須先調(diào)用父類(lèi)的 super
// 這里不需要
aCoder.encode(self.firstName, forKey: "first")
aCoder.encode(self.lastName, forKey: "last")
aCoder.encode(self.age, forKey: "age")
}
required init?(coder aDecoder: NSCoder) {
// 同上,如果存在父類(lèi)采用NSCoding協(xié)議,則也需要先調(diào)用父類(lèi)的構(gòu)造器
// 注意這里返回的是 NSString 類(lèi)型
self.firstName = aDecoder.decodeObject(of: NSString.self, forKey: "first")! as String
self.lastName = aDecoder.decodeObject(of: NSString.self, forKey: "last")! as String
// 對(duì)于Int類(lèi)型
self.age = aDecoder.decodeInteger(forKey: "age")
}
}
將數(shù)據(jù)存儲(chǔ)在本地的documents文件夾中的 person.txt 文件中
let fm = FileManager.default
// 獲取documents 文件夾所在的URL
let docsurl = try fm.url(.documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let personFile = docsurl.appendingPathComponent("person.txt")
let person = Person(firstName: "louis", lastName: "lili", age: 20)
// 將上面的person使用 NSKeyedArchiver 進(jìn)行存儲(chǔ)
// 先轉(zhuǎn)換為 NSData 類(lèi)型
// 如果使用 NSSecureCoding 協(xié)議, 則requiringSecureCoing則需要使用true
// 如果使用 NSCoding 協(xié)議, 則requiringSecureCoing為false
let personData = try NSKeyedArchiver.archivedData(withRootObject: person, requiringSecureCoing: true)
// 使用 write(to:) 方法寫(xiě)入文件
// 哪些數(shù)據(jù)類(lèi)型可以使用 write(to:) 方法,下面會(huì)介紹
personData.write(to: personFile, options: .atomic, encoding: .utf8)
NSString 和 NSData 對(duì)象可以直接的將內(nèi)容寫(xiě)入到文件中:
try "funny".write(to: someFileUrl/file.txt, atomically: true, encoding: .utf8)
對(duì)于 NSArray 和 NSDictionary,實(shí)際上他們是 屬性列表(property lists), 它們需要其包含的內(nèi)容都是 屬性列表類(lèi)型(property list types),這些類(lèi)型包括:
NSStringNSDataNSDateNSNumberNSArrayNSDictionary
如果是以上類(lèi)型則都可以直接寫(xiě)入文件,比如:
// 數(shù)組
let arr = ["hello", "world"]
let temp = FileManager.default.temporaryDirectory
let f = temp.appendingPathComponent("pep.plist")
// 轉(zhuǎn)化為 NSArray類(lèi)型
try (arr as NSArray).write(to: f)
回到正題,剛才將數(shù)據(jù)person轉(zhuǎn)化為 NSData 后寫(xiě)入了文件,下面是讀取數(shù)據(jù)的方法:
// 將 personFile 路徑下文件的內(nèi)容讀取為 NSData 格式
let personData = try NSData(contentsOf: personFile)
// 然后進(jìn)行解檔
// 注意這里的ofClass是 Person.self
// 如果存入的數(shù)據(jù)是 [Person]數(shù)組,則這里相對(duì)應(yīng)的則是 [Person].self
let personObj = try NSKeyedUnarchiver.unarchivedObject(ofClass: Person.self, from: personData)!
print(person) // louis lili 20
Codable
這個(gè)是swift4.0中引入的新協(xié)議,主要是為了解決數(shù)據(jù)(比如JSON)序列化問(wèn)題。它實(shí)際上是 Encodable 和 Decodable 協(xié)議的結(jié)合.
使用Codable的對(duì)象,類(lèi)實(shí)例,結(jié)構(gòu)體實(shí)例,枚舉實(shí)例(RawRepresentable 類(lèi)型的枚舉,即擁有 raw value)等都可以被編碼
protocol Codable: Encodable & Decodable {}
任何對(duì)象只要遵守Encodable協(xié)議,都可以被序列化(歸檔),任何遵循Decodable協(xié)議的對(duì)象都可以從序列化形式恢復(fù)(解檔)。
存在3種形式的序列化模式:
-
property list: 使用PropertyListEncoder的encode(_:)進(jìn)行編碼,使用PropertyListDecoder的decode(_:from:)進(jìn)行解碼 -
JSON: 使用JSONEncoder的encode(_:)進(jìn)行編碼,使用JSONDecoder的decode(_:from:)進(jìn)行解碼 -
NSCoder: 使用NSKeyedArchiverEncoder的encodeEncodable(_:forKey:)進(jìn)行編碼,使用NSKeyedUnarchiverDecoder的decodeDecodable(_:forKey:)進(jìn)行解碼
大多數(shù)內(nèi)置的Swift類(lèi)型都是默認(rèn)的Codable,encode(to:) 和 init(from:) 類(lèi)似于 NSCoding中的 encode(with:) 和 init(coder:), 但是通常不需要我們?nèi)?shí)現(xiàn),因?yàn)橥ㄟ^(guò)擴(kuò)展協(xié)議的方式,提供了默認(rèn)的實(shí)現(xiàn)
上面的存儲(chǔ) Person 實(shí)例的方式,這里可以寫(xiě)為:
// 不需要寫(xiě) encode(with:) 和 init(coder:) 的協(xié)議方法
// 因?yàn)閰f(xié)議擴(kuò)展 extension Codable 中提供了默認(rèn)實(shí)現(xiàn)
class Person: NSObject, Codable {
var firstName: String
var lastName: String
var age: Int
// 如果定義一個(gè)實(shí)例Person,打印結(jié)果將是這里定義的描述字符串
override var descirption: String {
return "\(self.firstName) \(self.lastName) \(age)"
}
init(firstName: String, lastName: String, age: Int) {
self.firstName = firstName
self.lastName = lastName
self.age = age
}
}
推薦使用 PropertyListEncoder & PropertyListDecoder, 這個(gè)的實(shí)現(xiàn)方式和 NSKeyedArchiver & NSKeyedUnarchiver 類(lèi)似
// 對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ)
let docsurl = try FileManager.default.url(for: .docmentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let filePath = docsurl.appendingPathComponent("person.txt")
let person = Perosn(firstName: "yuuki", lastName: "lili", age: 20)
let encodedPerson = try PropertyEncoder().encode(person) // 編碼
// 寫(xiě)入到該文件
encodedPerson.write(to: filePath, options: .atomic)
// 對(duì)數(shù)據(jù)進(jìn)行讀取
let contents = try Data(contentOf: filePath)
let decodedPerson = try PropertyListDecoder().decode(Person.self, from: contents) // 解碼
print(decodedPerson) // "yuuki lili 20"
示例
主要有以下幾個(gè)方面:
- NSCoding 和 Codable 結(jié)合使用,因?yàn)镃ocoa中很多類(lèi)采用了
NSCoding協(xié)議,而不是 Codable協(xié)議,有時(shí)候需要將2者結(jié)合起來(lái)一起用 - http請(qǐng)求返回的JSON數(shù)據(jù)的編碼和解碼
- 使用
CodingKeys枚舉都字段進(jìn)行自定義命名
示例1.使用Codable存儲(chǔ)NSCoding數(shù)據(jù)
比如 UIColor, UIImage 都使用的NSCoding協(xié)議,比如存儲(chǔ)下面數(shù)據(jù)
struct Person {
var name: String
var favoriteColor: UIColor
}
這需要自己手動(dòng)實(shí)現(xiàn) init(from:) 和 encode(to:) 協(xié)議方法,并且使用 CodingKeys 對(duì) key 一個(gè)接一個(gè)的進(jìn)行匹配。
需要4個(gè)步驟:
- 擴(kuò)展
Person, 存放Codable功能 - 創(chuàng)建自定義coding keys,用來(lái)描述存儲(chǔ)的數(shù)據(jù)是什么
- 創(chuàng)建一個(gè)
init(from:)方法,將原始數(shù)據(jù)轉(zhuǎn)換回一個(gè)UIColor, 使用NSKeyedUnarchiver進(jìn)行解檔 - 創(chuàng)建一個(gè)
encode(to:)方法,將UIColor轉(zhuǎn)化為原始數(shù)據(jù), 使用NSKeyedArchiver進(jìn)行歸檔
上面的3 & 4步驟前面說(shuō)過(guò),對(duì)于所有采用Codable的類(lèi)型一般可以省略,這里需要自己實(shí)現(xiàn)轉(zhuǎn)換
extension Person: Codable {
// 因?yàn)槲覀冃枰@示的聲明編碼和解碼的內(nèi)容
// 因此需要在這里寫(xiě)出CodingKeys
// CodingKeys 遵循 String, CodingKey
enum CodingKeys: String, CodingKey {
case name
case favoriteColor
}
init(from decoder: Decoder) throws {
// 注意這里的 keyedBy
let container = try decoder.container(keyedBy: CodingKeys.self)
// 字符串類(lèi)型,直接解碼
name = try container.decode(String.self, forKey: .name)
let colorData = try container.decode(Data.self, forKey: .favoriteColor)
favoriteColor = try NSKeyedUnarchiver.unarchiverTopLevelObjectWithData(colorData) as? UIColor ?? UIColor.black
}
func encode(to encoder: Encoder) throws {
let container = try encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
let colorData = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: false)
try container.encode(colorData, forKey: .favoriteColor)
}
}
// 使用
let taylor = Person(name: "Taylor Swift", favoriteColor: .blue)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
do {
// 編碼
let encoded = try encoder.encode(taylor)
let str = String(decoding: encoded, as: UTF8.self)
print(str)
// 解碼
let person = try decoder.decode(Person.self, from: encoded)
print(person.favoriteColor, person.name)
} catch {
print(error,localizedDescription)
}
示例來(lái)源:
示例2:Decodable & Encodable
假設(shè)網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)是一個(gè)json,格式:
1.返回一個(gè)普通的字典
{
"id":1,
"name":"Instagram Firebase",
"link":"https://www.letsbuildthatapp.com/course/instagram-firebase",
"imageUrl":"https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/04782e30-d72a-4917-9d7a-c862226e0a93",
"number_of_lessons":49
}
這個(gè)可以定義一個(gè)結(jié)構(gòu)體,使用 JSONDecoder 實(shí)例的 decode 方法對(duì)返回的數(shù)據(jù)進(jìn)行解析:
// 這個(gè)結(jié)構(gòu)體遵循 Decodable協(xié)議
struct Course: Decodable {
let id: Int
let name: String
let link: String
let imageUrl: String
let number_of_lessons: Int
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let urlStr = "https://api.letsbuildthatapp.com/jsondecodable/course"
guard let url = URL(string: urlStr) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
// 使用 JSONDecoder對(duì)數(shù)據(jù)進(jìn)行解析
let course = try JSONDecoder().decode(Course.self, from: data)
print("course", course)
} catch {
print(error.localization)
}
}
task.resume()
}
}
2.如果返回的數(shù)據(jù)是一個(gè)
Course數(shù)組:
// 20190506205007
// https://api.letsbuildthatapp.com/jsondecodable/courses
[
{
"id": 1,
"name": "Instagram Firebase",
"link": "https://www.letsbuildthatapp.com/course/instagram-firebase",
"imageUrl": "https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/04782e30-d72a-4917-9d7a-c862226e0a93",
"number_of_lessons": 49
},
{
"id": 2,
"name": "Podcasts Course",
"link": "https://www.letsbuildthatapp.com/course/podcasts",
"imageUrl": "https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/32f98d9d-5b9b-4a22-a012-6b87fd7158c2",
"number_of_lessons": 39
}
]
則上面需要改動(dòng)的地方為:
// Course.self 更改為 [Course].self
let courses = try JSONDecoder().decode([Course].self, from: data)
3.如果返回?cái)?shù)據(jù)類(lèi)型是多種數(shù)據(jù)類(lèi)型組合
// 20190506205524
// https://api.letsbuildthatapp.com/jsondecodable/website_description
{
"name": "Lets Build That App",
"description": "Teaching and Building Apps since 1999",
"courses": [
{
"id": 1,
"name": "Instagram Firebase",
"link": "https://www.letsbuildthatapp.com/course/instagram-firebase",
"imageUrl": "https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/04782e30-d72a-4917-9d7a-c862226e0a93",
"number_of_lessons": 49
},
{
"id": 4,
"name": "Kindle Basic Training",
"link": "https://www.letsbuildthatapp.com/basic-training",
"imageUrl": "https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/a6180731-c077-46e7-88d5-4900514e06cf",
"number_of_lessons": 19
}
]
}
則需要再定義一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體也使用 Decodable 協(xié)議:
struct Website: Decoable {
let name: String
let description: String
let courses: [Course] // 可以進(jìn)行組合
}
則修改部分:
// [Course].self 更改為 Website.self
let website = try JSONDecoder().decode(Website.self, from: data)
4.如果返回的數(shù)據(jù)某些可能為空
// 20190506210445
// https://api.letsbuildthatapp.com/jsondecodable/courses_missing_fields
[
{
"id": 1,
"name": "Instagram Firebase",
"link": "https://www.letsbuildthatapp.com/course/instagram-firebase",
"imageUrl": "https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/04782e30-d72a-4917-9d7a-c862226e0a93",
"number_of_lessons": 49
},
{
"id": 4,
"name": "Kindle Basic Training",
"link": "https://www.letsbuildthatapp.com/basic-training",
"imageUrl": "https://letsbuildthatapp-videos.s3-us-west-2.amazonaws.com/a6180731-c077-46e7-88d5-4900514e06cf",
"number_of_lessons": 19
},
{
"name": "Yelp"
}
]
則需要將結(jié)構(gòu)體中某些類(lèi)型定義為可選類(lèi)型
struct Course: Decodable {
let id: Int? // 可選類(lèi)型
let name: String
let link: String?
let number_of_lessons: Int?
}
將其修改為:
let courses = try JSONDecoder().decode([Course].self, from: data)
示例來(lái)源:
上面的示例只對(duì)獲取到的數(shù)據(jù)進(jìn)行了解析,如果要上傳數(shù)據(jù),則需要使用到 JSONEncoder 實(shí)例的 encode 方法對(duì)數(shù)據(jù)進(jìn)行編碼操作, 過(guò)程和上面示例的過(guò)程類(lèi)型, 可參考:
3.CodingKeys
上面的示例,Course 結(jié)構(gòu)體的屬性名要和后臺(tái)保持一致,如果想要自定義屬性名,則需要添加 CodingKeys 枚舉,上面 示例1 中其實(shí)已經(jīng)出現(xiàn)過(guò)了,這里單獨(dú)拿出來(lái)說(shuō)明一下:
// 原來(lái)的
struct Course: Decodable {
let id: Int
let name: String
let link: String
let imageUrl: String
let number_of_lessons: Int
}
// 將 name,link, number_of_lessons 屬性分別進(jìn)行修改
struct Course: Decodable {
let id: Int
let courseName: String
let courseLink: String
let imageUrl: String
let courseCount: Int
// CodingKeys 遵循String 和 CodingKey 協(xié)議
enum CodingKeys: String, CodingKey {
case id, imageUrl
case courseName = "name"
case courseLink = "link"
case courseCount = "number_of_lessons"
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let urlStr = "https://api.letsbuildthatapp.com/jsondecodable/course"
guard let url = URL(string: urlStr) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
// 使用 JSONDecoder對(duì)數(shù)據(jù)進(jìn)行解析
let course = try JSONDecoder().decode(Course.self, from: data)
print("course", course.courseCount) // 使用自定義的屬性名
} catch {
print(error.localization)
}
}
task.resume()
}
}
總結(jié)
這些協(xié)議對(duì)數(shù)據(jù)結(jié)構(gòu)的處理,存儲(chǔ)還是很重要的,主要涉及知識(shí)點(diǎn):
NSCoding & NSSecureCodingNSObjectCodable & Encodable & DecodablePropertyListEncoder & PropertyListDecoderNSKeyedArchiver & NSKeyedUnarchiverJSONEncoder & JSONDecoderNSCoder
另外還提到了:
-
Property List & Property List typesNSDataNSStringNSNumberNSArrayNSDictionary
FileManagerURLSession網(wǎng)絡(luò)請(qǐng)求
2019年05月06日21:26:46