Swift Codable 和 NSCoding协议,以及归档,JSON编码

前面文章中 UserDefaults 的基本用法 中对UserDefaults 进行了简单的介绍,它可以将一些简单的数据类型存储在本地,需要使用的时候再去读取。

如果对于复杂对象的存储则需要将其进行序列化,将对象转化为 NSData(Swift Data)类型之后再进行操作,比如,将其存在本地的某个文件(eg.people.plist, people.txt等)中。

有2种序列化的方式:

  1. NSCoding: 老的Cocoa方式,OC的方式
  2. Codable: 新的swift方式

NSCoding

这个协议在Cocoa的Foundation框架中定义,内置的大多数Cocoa类都采用了NSCoding协议,比如 UIColor 等。

采用了这个协议的对象可以转换为 NSData 类型,然后再转换回来。使用 NSKeyedArchiverNSKeyedUnarchiver 分别进行归档和解档。

采用这个协议的对象需要实现 encode(with:) 进行归档,以及 init(coder:) 进行解档。

比如,自定义的 Person 类,有2点值得说明的, 来源NSCoding - hackingwithswift

  • 为什么使用class,而不是struct? 因为NSCoding需要使用对象,或者在字符串,数组,字典的情况下,使用可以与对象互换的结构,如果把Person当做一个struct,我们不能在NSCoding中使用它
  • 为什么要继承 NSObject? 因为使用NSCoding,必须使用NSObject,否则应用会崩溃
class Person: NSObject, NSCoding {
    var firstName: String
    var lastName: String
    var age: Int
    
    // 如果定义一个实例Person,打印结果将是这里定义的描述字符串
    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
    }
    
    // 实现NSCoding 协议中的方法
    func encode(with aCoder: NSCoder) {
        // 如果Person 还有一个父类,假设Human也采用了NSCoding协议
        // 则必须先调用父类的 super
        // 这里不需要
        aCoder.encode(self.firstName, forKey: "first")
        aCoder.encode(self.lastName, forKey: "last")
        aCoder.encode(self.age, forKey: "age")
    }
    
    required init?(coder aDecoder: NSCoder) {
        // 同上,如果存在父类采用NSCoding协议,则也需要先调用父类的构造器
        
        // 注意这里返回的是 NSString 类型
        self.firstName = aDecoder.decodeObject(of: NSString.self, forKey: "first")! as String
        self.lastName = aDecoder.decodeObject(of: NSString.self, forKey: "last")! as String
        // 对于Int类型
        self.age = aDecoder.decodeInteger(forKey: "age")
    }
}

iOS12中,苹果推荐使用 NSSecureCoding 协议,这个协议在NSCoding的基础上,还需要实现一个静态的 static var supportsSecureCoding: Bool {return true} 属性

class Person: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool { return true } // 需要添加这个静态属性
    
    var firstName: String
    var lastName: String
    var age: Int
    
    // 如果定义一个实例Person,打印结果将是这里定义的描述字符串
    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
    }
    
    // 实现NSCoding 协议中的方法
    func encode(with aCoder: NSCoder) {
        // 如果Person 还有一个父类,假设Human也采用了NSCoding协议
        // 则必须先调用父类的 super
        // 这里不需要
        aCoder.encode(self.firstName, forKey: "first")
        aCoder.encode(self.lastName, forKey: "last")
        aCoder.encode(self.age, forKey: "age")
    }
    
    required init?(coder aDecoder: NSCoder) {
        // 同上,如果存在父类采用NSCoding协议,则也需要先调用父类的构造器
        
        // 注意这里返回的是 NSString 类型
        self.firstName = aDecoder.decodeObject(of: NSString.self, forKey: "first")! as String
        self.lastName = aDecoder.decodeObject(of: NSString.self, forKey: "last")! as String
        // 对于Int类型
        self.age = aDecoder.decodeInteger(forKey: "age")
    }
}

将数据存储在本地的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 进行存储
// 先转换为 NSData 类型
// 如果使用 NSSecureCoding 协议, 则requiringSecureCoing则需要使用true
// 如果使用 NSCoding 协议, 则requiringSecureCoing为false
let personData = try NSKeyedArchiver.archivedData(withRootObject: person, requiringSecureCoing: true)

// 使用 write(to:) 方法写入文件
// 哪些数据类型可以使用 write(to:) 方法,下面会介绍
personData.write(to: personFile, options: .atomic, encoding: .utf8)

NSStringNSData 对象可以直接的将内容写入到文件中:

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

对于 NSArrayNSDictionary,实际上他们是 属性列表(property lists), 它们需要其包含的内容都是 属性列表类型(property list types),这些类型包括:

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

如果是以上类型则都可以直接写入文件,比如:

// 数组
let arr = ["hello", "world"]
let temp = FileManager.default.temporaryDirectory
let f = temp.appendingPathComponent("pep.plist")
// 转化为 NSArray类型
try (arr as NSArray).write(to: f)

回到正题,刚才将数据person转化为 NSData 后写入了文件,下面是读取数据的方法:

// 将 personFile 路径下文件的内容读取为 NSData 格式
let personData = try NSData(contentsOf: personFile)
// 然后进行解档
// 注意这里的ofClass是 Person.self
// 如果存入的数据是 [Person]数组,则这里相对应的则是 [Person].self
let personObj = try NSKeyedUnarchiver.unarchivedObject(ofClass: Person.self, from: personData)!

print(person) // louis lili 20

Codable

这个是swift4.0中引入的新协议,主要是为了解决数据(比如JSON)序列化问题。它实际上是 EncodableDecodable 协议的结合.

使用Codable的对象,类实例,结构体实例,枚举实例(RawRepresentable 类型的枚举,即拥有 raw value)等都可以被编码

protocol Codable: Encodable & Decodable {}

任何对象只要遵守Encodable协议,都可以被序列化(归档),任何遵循Decodable协议的对象都可以从序列化形式恢复(解档)。

存在3种形式的序列化模式:

  • property list: 使用 PropertyListEncoderencode(_:) 进行编码,使用 PropertyListDecoderdecode(_:from:) 进行解码
  • JSON: 使用 JSONEncoderencode(_:) 进行编码,使用 JSONDecoderdecode(_:from:) 进行解码
  • NSCoder: 使用 NSKeyedArchiverEncoderencodeEncodable(_:forKey:) 进行编码,使用 NSKeyedUnarchiverDecoderdecodeDecodable(_:forKey:) 进行解码

大多数内置的Swift类型都是默认的Codable,encode(to:)init(from:) 类似于 NSCoding中的 encode(with:)init(coder:), 但是通常不需要我们去实现,因为通过扩展协议的方式,提供了默认的实现

上面的存储 Person 实例的方式,这里可以写为:

// 不需要写 encode(with:) 和 init(coder:) 的协议方法
// 因为协议扩展 extension Codable 中提供了默认实现
class Person: NSObject, Codable {
    var firstName: String
    var lastName: String
    var age: Int
    
    // 如果定义一个实例Person,打印结果将是这里定义的描述字符串
    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, 这个的实现方式和 NSKeyedArchiver & NSKeyedUnarchiver 类似

// 对数据进行存储
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) // 编码
// 写入到该文件
encodedPerson.write(to: filePath, options: .atomic)


// 对数据进行读取
let contents = try Data(contentOf: filePath)
let decodedPerson = try PropertyListDecoder().decode(Person.self, from: contents) // 解码
print(decodedPerson) // "yuuki lili 20"

示例

主要有以下几个方面:

  • NSCoding 和 Codable 结合使用,因为Cocoa中很多类采用了 NSCoding 协议,而不是 Codable协议,有时候需要将2者结合起来一起用
  • http请求返回的JSON数据的编码和解码
  • 使用CodingKeys枚举都字段进行自定义命名

示例1.使用Codable存储NSCoding数据

比如 UIColor, UIImage 都使用的NSCoding协议,比如存储下面数据

struct Person {
    var name: String
    var favoriteColor: UIColor
}

这需要自己手动实现 init(from:)encode(to:) 协议方法,并且使用 CodingKeyskey 一个接一个的进行匹配。

需要4个步骤:

  1. 扩展 Person, 存放 Codable 功能
  2. 创建自定义coding keys,用来描述存储的数据是什么
  3. 创建一个 init(from:) 方法,将原始数据转换回一个 UIColor, 使用 NSKeyedUnarchiver 进行解档
  4. 创建一个 encode(to:) 方法,将UIColor转化为原始数据, 使用 NSKeyedArchiver 进行归档

上面的3 & 4步骤前面说过,对于所有采用Codable的类型一般可以省略,这里需要自己实现转换

extension Person: Codable {
    // 因为我们需要显示的声明编码和解码的内容
    // 因此需要在这里写出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)
        // 字符串类型,直接解码
        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)
}

示例来源:

示例2:Decodable & Encodable

假设网络请求返回的数据是一个json,格式:

1.返回一个普通的字典

{
    "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
}

这个可以定义一个结构体,使用 JSONDecoder 实例的 decode 方法对返回的数据进行解析:

// 这个结构体遵循 Decodable协议
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对数据进行解析
                 let course = try JSONDecoder().decode(Course.self, from: data)
                 print("course", course)
             } catch {
                 print(error.localization)
             }
        }
        task.resume()
    }
}

2.如果返回的数据是一个 Course 数组:

// 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
  }
]

则上面需要改动的地方为:

// Course.self 更改为 [Course].self
let courses = try JSONDecoder().decode([Course].self, from: data)

3.如果返回数据类型是多种数据类型组合

// 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
    }
  ]
}

则需要再定义一个结构体,这个结构体也使用 Decodable 协议:

struct Website: Decoable {
    let name: String
    let description: String
    let courses: [Course] // 可以进行组合
}

则修改部分:

// [Course].self 更改为 Website.self
let website = try JSONDecoder().decode(Website.self, from: data)

4.如果返回的数据某些可能为空

// 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"
  }
]

则需要将结构体中某些类型定义为可选类型

struct Course: Decodable {
    let id: Int? // 可选类型
    let name: String
    let link: String?
    let number_of_lessons: Int?
}

将其修改为:

let courses = try JSONDecoder().decode([Course].self, from: data)

示例来源:

上面的示例只对获取到的数据进行了解析,如果要上传数据,则需要使用到 JSONEncoder 实例的 encode 方法对数据进行编码操作, 过程和上面示例的过程类型, 可参考:

3.CodingKeys

上面的示例,Course 结构体的属性名要和后台保持一致,如果想要自定义属性名,则需要添加 CodingKeys 枚举,上面 示例1 中其实已经出现过了,这里单独拿出来说明一下:

// 原来的
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 属性分别进行修改
struct Course: Decodable {
    let id: Int
    let courseName: String
    let courseLink: String
    let imageUrl: String
    let courseCount: Int
    
    // CodingKeys 遵循String 和 CodingKey 协议
    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对数据进行解析
                 let course = try JSONDecoder().decode(Course.self, from: data)
                 print("course", course.courseCount) // 使用自定义的属性名
             } catch {
                 print(error.localization)
             }
        }
        task.resume()
    }
}

总结

这些协议对数据结构的处理,存储还是很重要的,主要涉及知识点:

  • 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 网络请求

2019年05月06日21:26:46

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容