美文网首页Swift项目相关
Swift自定义类的存储

Swift自定义类的存储

作者: topws1 | 来源:发表于2018-05-23 15:08 被阅读50次

    在Swift开发中,许多时候会涉及到存储自定义的类,不管是存储到本地文件还是远程服务器,都会涉及到编码和解码的问题。下面就来介绍一下在Swift中怎么存储自定义的类。
    在Swift中存储自定义的类有两种方法,一种是Swift 3.0版本的NSCoding,还有一种是Swift 4.0版本的Codable。我们就两种方法来谈谈其中的区别吧。

    Swift 3.0: NSCoding

    在NSCoding这个协议在OC中就存在,所以自然而然的,Swift也能使用它。使用NSCoding协议,需要这个类继承自NSObject并遵循NSCoding协议,而且还要实现NSCoding协议中的两个方法,才能编码和解码自定义的类。例子如下:

    class Person: NSObject, NSCoding {
        var name: String
        var age: Int
    
        init(name: String, age: Int) {
            self.name = name
            self.age = age
        }
    
        //编码
        func encode(with aCoder: NSCoder) {
            aCoder.encode(name, forKey: "name")
            aCoder.encode(age, forKey: "age")
        }
    
        //解码
        required init?(coder aDecoder: NSCoder) {
            self.name = aDecoder.decodeObject(forKey: "name") as! String
            self.age = aDecoder.decodeInteger(forKey: "age")
        }
    }
    

    其中编码和解码两个方法就是NSCoding协议中要求实现的两个方法,只有实现了这两个方法,才能将自定义的类序列化和反序列化。接下来,我们就要创建一个Person类的实例,并保存到本地文件中,和从本地文件中读取内容。

    let person = Person(name: "Tom", age: 18)
    
    //拿到一个本地文件的URL
    let manager = FileManager.default
    var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
    url?.appendPathComponent("test.txt")
    
    //序列化Person实例
    let dataWrite = NSKeyedArchiver.archivedData(withRootObject: person)
    
    do {
        //将数据写入文件
        try dataWrite.write(to: url!)
    } catch {
        print("保存到本地文件失败")
    }
    
    //从本地文件读取数据
    if let dataRead = try? Data(contentsOf: url!) {
        //反序列化数据,并将结果强转为Person类的实例
        let person = NSKeyedUnarchiver.unarchiveObject(with: dataRead) as! Person
        //输出内容
        print("name: \(person.name)")  //输出: name: Tom
        print("age: \(person.age)")  //输出: age: 18
    } else {
        print("文件不存在,读取本地文件失败")
    }
    

    可以看出,当第一步完成之后,保存自定义类就变得非常简单了,只需要使用系统定义好的NSKeyedArchiver类和NSKeyedUnarchiver类就可以了。

    Swift 4.0: Codable

    接下来再看看Swift 4.0的Codable协议怎么保存自定义类。
    在Swift 4.0版本,Apple推出了一个新的协议叫Codable。只要类或者结构体遵循了这个协议,就能够很方便地将类或者结构体的实例转换为JSON数据,并且不需要在类或者结构体中实现编码和解码方法,可以看出,Codable协议比NSCoding协议更加方便、简洁。废话不多说,来看看具体的实现:

    class Person: Codable {
        var name: String
        var age: Int
    
        init(name: String, age: Int) {
            self.name = name
            self.age = age
        }
    }
    
    let person = Person(name: "Tom", age: 18)
    
    //拿到一个本地文件的URL
    let manager = FileManager.default
    var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
    url?.appendPathComponent("test.txt")
    
    //将Person实例转换为JSON数据
    let dataWrite = try JSONEncoder().encode(person)
    do {
        try dataWrite.write(to: url!)
    } catch {
        print("保存到本地文件失败")
    }
    
    //从本地文件读取数据
    if let dataRead = try? Data(contentsOf: url!) {
        //将数据转换为Person实例
        let person = try JSONDecoder().decode(Person.self, from: dataRead)
        //输出内容
        print("name: \(person.name)")  //输出: name: Tom
        print("age: \(person.age)")  //输出: age: 18
    } else {
        print("文件不存在,读取本地文件失败")
    }
    

    可以看出,Codable协议比NSCoding协议方便很多,而且代码看起来也简洁了很多。

    Codable的坑

    那可能就有人想问了,既然有了Codable协议,而且还这么简洁,那么NSCoding协议的存在还有什么意义呢?这个问题博主当初也想了很久,后来在一次偶然的机会,发现了Codable协议的不足。先卖个关子,我们来看一个需求:
    要求:现有一个AnimalProtocol协议,该协议有一个eat()方法,然后实现两个类Cat和Dog,这两个类都有一个类型为String的name属性,并且需要遵循AnimalProtocol协议,实现eat()方法。创建一个数组,数组中保存若干个Cat实例和Dog实例,将此数组保存到本地文件中,并从文件中读取到相应的内容。

    先来看看NSCoding协议是怎么实现这个需求的:

    //定义协议
    protocol AnimalProtocol {
        func eat()
    }
    
    //定义Cat类
    class Cat: NSObject, NSCoding, AnimalProtocol {
        var name: String
    
        init(name: String) {
            self.name = name
        }
    
        func eat() {
            print("cat \(self.name) eat")
        }
    
        func encode(with aCoder: NSCoder) {
            aCoder.encode(name, forKey: "name")
        }
    
        required init?(coder aDecoder: NSCoder) {
            self.name = aDecoder.decodeObject(forKey: "name") as! String
        }
    }
    
    //定义Dog类
    class Dog: NSObject, NSCoding, AnimalProtocol {
        var name: String
    
        init(name: String) {
            self.name = name
        }
    
        func eat() {
            print("dog \(self.name) eat")
        }
    
        func encode(with aCoder: NSCoder) {
            aCoder.encode(name, forKey: "name")
        }
    
        required init?(coder aDecoder: NSCoder) {
            self.name = aDecoder.decodeObject(forKey: "name") as! String
        }
    }
    
    //创建一个空数组(因为这个数组要同时保存两个类的实例,所以需要以它们共同的一个类型来创建数组)
    var animalArray = [AnimalProtocol]()
    
    //然后用循环创建多个Cat和Dog实例,并加入数组中
    for i in 1...3 {
        let cat = Cat(name: "cat\(i)")
        let dog = Dog(name: "dog\(i)")
    
        animalArray.append(cat)
        animalArray.append(dog)
    }
    
    let manager = FileManager.default
    var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
    url?.appendPathComponent("test.txt")
    
    let dataWrite = NSKeyedArchiver.archivedData(withRootObject: animalArray)
    do {
        try dataWrite.write(to: url!)
    } catch {
        print("保存到本地文件失败")
    }
    
    if let dataRead = try? Data(contentsOf: url!) {
        let newArray = NSKeyedUnarchiver.unarchiveObject(with: dataRead) as! [AnimalProtocol]
    
        for item in newArray {
            item.eat()
        }
    }
    //输出如下
    /*
    cat cat1 eat
    dog dog1 eat
    cat cat2 eat
    dog dog2 eat
    cat cat3 eat
    dog dog3 eat
    */
    

    可以看到,虽然步骤有一点点麻烦,但是还是能很方便地实现我们的需求的,接下来,我们再看看Codable协议是怎么做的吧。
    先上代码:

    //定义协议
    protocol AnimalProtocol: Codable {
        func eat()
    }
    
    //定义Cat类
    class Cat: AnimalProtocol {
        var name: String
    
        init(name: String) {
            self.name = name
        }
    
        func eat() {
            print("cat \(self.name) eat")
        }
    }
    
    //定义Dog类
    class Dog: AnimalProtocol {
        var name: String
    
        init(name: String) {
            self.name = name
        }
    
        func eat() {
            print("dog \(self.name) eat")
        }
    }
    
    var animalArray = [AnimalProtocol]()
    
    for i in 1...3 {
        let cat = Cat(name: "cat\(i)")
        let dog = Dog(name: "dog\(i)")
    
        animalArray.append(cat)
        animalArray.append(dog)
    }
    
    let manager = FileManager.default
    var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
    url?.appendPathComponent("test.txt")
    
    let dataWrite = try JSONEncoder().encode(animalArray)
    
    do {
        try dataWrite.write(to: url!)
    } catch {
        print("保存到本地文件失败")
    }
    
    if let dataRead = try? Data(contentsOf: url!) {
        let newArray = try JSONDecoder().decode([AnimalProtocol].self, from: dataRead)
    
        for item in newArray {
            item.eat()
        }
    } else {
        print("文件不存在,读取本地文件失败")
    }
    

    看代码没错,而且能通过编译,但是一运行,直接crash。还报了一个莫名其妙的错误,错误是这样写的:fatal error: Array does not conform to Encodable because AnimalProtocol does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.65/src/swift/stdlib/public/core/Codable.swift, line 3962
    2017-10-14 19:43:37.565213+0800 study[5383:411423] fatal error: Array does not conform to Encodable because AnimalProtocol does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.65/src/swift/stdlib/public/core/Codable.swift, line 3962
    我去!这就郁闷了,明明我的类遵循了Codable协议,你却说没有遵循,这是什么鬼,而且就算用扩展让Array遵循协议也不可以

    extension Array: Codable {
    }
    

    难道是遇到鬼了?不会吧!
    后来博主请教导师,才知道原来这是Codable的一个不足点。遵循了Codable协议的类的数组,其中的元素必须为同一个类型。虽然这里是用协议类型去创建的数组,但其元素却是遵循了该协议类型的类的实例。所以本质上不尽相同。

    如果在这里将AnimalProtocol协议改成一个类的话,虽然在运行时不会崩溃,但却无法将其向下转换为其子类了。代码如下:

    //定义父类
    class Animal: Codable {
        func eat() {
            print("Animal")
        }
    
    }
    
    //定义Cat类
    class Cat: Animal {
        var name: String
    
        init(name: String) {
            self.name = name
            super.init()
        }
    
        required init(from decoder: Decoder) throws {
            fatalError("init(from:) has not been implemented")
        }
    
        override func eat() {
            print("cat \(self.name) eat")
        }
    }
    
    //定义Dog类
    class Dog: Animal {
        var name: String
    
        init(name: String) {
            self.name = name
            super.init()
        }
    
        required init(from decoder: Decoder) throws {
            fatalError("init(from:) has not been implemented")
        }
    
        override func eat() {
            print("dog \(self.name) eat")
        }
    }
    
    var animalArray = [Animal]()
    
    for i in 1...3 {
        let cat = Cat(name: "cat\(i)")
        let dog = Dog(name: "dog\(i)")
    
        animalArray.append(cat)
        animalArray.append(dog)
    }
    
    let manager = FileManager.default
    var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
    url?.appendPathComponent("test.txt")
    
    let dataWrite = try JSONEncoder().encode(animalArray)
    
    do {
        try dataWrite.write(to: url!)
    } catch {
        print("保存到本地文件失败")
    }
    
    if let dataRead = try? Data(contentsOf: url!) {
        let newArray = try JSONDecoder().decode([Animal].self, from: dataRead)
    
        for item in newArray {
            if item is Cat {
                (item as! Cat).eat()
            } else if item is Dog {
                (item as! Dog).eat()
            } else {
                item.eat()
            }
        }
    } else {
        print("文件不存在,读取本地文件失败")
    }
    /*
    输出结果为:
    Animal
    Animal
    Animal
    Animal
    Animal
    Animal
    */
    

    以上便是博主个人的一些看法,如有错误,还请各位看客纠正。

    本文转自:https://blog.csdn.net/average17/article/details/78236589

    相关文章

      网友评论

        本文标题:Swift自定义类的存储

        本文链接:https://www.haomeiwen.com/subject/pumrjftx.html