Swift Codable 和 NSCoding協(xié)議,以及歸檔,JSON編碼

前面文章中 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種序列化的方式:

  1. NSCoding: 老的Cocoa方式,OC的方式
  2. 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)。使用 NSKeyedArchiverNSKeyedUnarchiver 分別進(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)

NSStringNSData 對(duì)象可以直接的將內(nèi)容寫(xiě)入到文件中:

try "funny".write(to: someFileUrl/file.txt, atomically: true, encoding: .utf8)

對(duì)于 NSArrayNSDictionary,實(shí)際上他們是 屬性列表(property lists), 它們需要其包含的內(nèi)容都是 屬性列表類(lèi)型(property list types),這些類(lèi)型包括:

  • NSString
  • NSData
  • NSDate
  • NSNumber
  • NSArray
  • NSDictionary

如果是以上類(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í)際上是 EncodableDecodable 協(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: 使用 PropertyListEncoderencode(_:) 進(jìn)行編碼,使用 PropertyListDecoderdecode(_:from:) 進(jìn)行解碼
  • JSON: 使用 JSONEncoderencode(_:) 進(jìn)行編碼,使用 JSONDecoderdecode(_:from:) 進(jìn)行解碼
  • NSCoder: 使用 NSKeyedArchiverEncoderencodeEncodable(_:forKey:) 進(jìn)行編碼,使用 NSKeyedUnarchiverDecoderdecodeDecodable(_: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è)步驟:

  1. 擴(kuò)展 Person, 存放 Codable 功能
  2. 創(chuàng)建自定義coding keys,用來(lái)描述存儲(chǔ)的數(shù)據(jù)是什么
  3. 創(chuàng)建一個(gè) init(from:) 方法,將原始數(shù)據(jù)轉(zhuǎn)換回一個(gè) UIColor, 使用 NSKeyedUnarchiver 進(jìn)行解檔
  4. 創(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 & NSSecureCoding
  • NSObject
  • Codable & Encodable & Decodable
  • PropertyListEncoder & PropertyListDecoder
  • NSKeyedArchiver & NSKeyedUnarchiver
  • JSONEncoder & JSONDecoder
  • NSCoder

另外還提到了:

  • Property List & Property List types

    • NSData
    • NSString
    • NSNumber
    • NSArray
    • NSDictionary
  • FileManager

  • URLSession 網(wǎng)絡(luò)請(qǐng)求

2019年05月06日21:26:46

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容