在Swift中实现数据存储的最佳方式是什么,如何处理类变化

问题描述

我是这里的一个新的swift程序员,正在用自己的项目实践,在实现数据存储的时候发现有很多方法可以做到,比如UserDefaults、Plist、CoreData…… 我选择了 Plist 作为我自己的数据持久化方法,我立即发现有一个问题,要存储那些自定义类,我需要使其遵循 Codable 协议。

例如,我的自定义类 User 有变量和函数


class User: Codable {
  
   var name: String
   var gender: Gender
   var avatar: Data
   var keys: Int
   var items: Array<Item>
   var vip: Bool
   
   var themeColorSetting: ThemeColor? = nil

   
   public init(name: String,gender: Gender,avatar: UIImage,keys: Int,items: Array<Item>,vip: Bool) {
       self.name = name
       self.gender = gender
       self.avatar = avatar.pngData() ?? Data()
       self.keys = keys
       self.items = items
       self.vip = vip
   }
   
   public func getAvatarImage() -> UIImage {
       return UIImage(data: avatar) ?? UIImage()
   }
   
   
}

当我将它存储到 Plist 时,这工作正常, 但是当我尝试添加一个函数

public func setAvatarImage(_ image: UIImage) {

        avatar = image.pngData() ?? #imageLiteral(resourceName: "Test").pngData()!
    }

到了类,我发现无法读取原始数据,因为它在编码文件中没有新功能,这甚至导致当我将新构建上传到TestFlight时崩溃,

那么,即使将来我添加新变量或函数时,存储仍然有效的数据的最佳方法是什么,或者您如何处理类的重构或扩展。 非常感谢

更新User类导致的崩溃问题:

我有

Thread 1: EXC_BREAKPOINT (code=1,subcode=0x10355e3a8)
class AppManager {
  public static let shared = AppEngine()
public var currentUser: User = User(name: "User",gender: .undefined,avatar: #imageLiteral(resourceName: "Test"),keys: 3,items: [Item](),vip: false)
  public let dataFilePath: URL? = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first?.appendingPathComponent("item.plist")

  init() {
    loadUser()
  }

 public func loadUser() {
        
        if let data = try? Data(contentsOf: dataFilePath!) {
           let decoder = JSONDecoder() //PropertyListDecoder()

           do {
                self.currentUser = try decoder.decode(User.self,from: data) 
           } catch {
               print(error)
           }
        }
        
        if self.currentUser.vip {
            print("Welcome back VIP!")
        }
    }

}

当我不调用 loadUser() 时,它工作正常,因为 AppManager 中有一个存储的用户,这一切都发生在我只是简单地向用户添加一个函数时,如果我删除应用程序并重新安装它,它在 loadUser() 调用时正常工作

解决方法

尽管您的代码和描述表明您只添加了方法,而不是属性,但简单地向 User 添加一个方法不应改变 PropertyListDecoderJSONDecoder 对其进行解码的能力。但是,如果您确实添加了存储属性,如果您更改了其他 Codable 属性,例如 ItemGender,您很可能无法从这些类型的先前版本编码的数据中对其进行解码。

跟踪它并为将来可能对 User 的存储属性进行的更改提供一些向后兼容性的一种方法是在 {{1} 中完全实现 Codable 协议而不是依赖 Swift 为您合成的一个。

User 基本上只是另外两个协议 CodableEncodable 的组合。

Decodable 需要一个函数 Encodableencode(to: Encoder) throws 需要一个初始化程序,Decodable

init(from: Decoder) throwss 将您的类序列化为“对象”类型,它本质上是一个字典。因此,您需要一个类型作为该字典的键,并且它需要符合 Encoder 协议。对于要序列化的每个属性,它应该有一个特定的值。通常,它是作为 CodingKey 实现的。

对于您的 enum 类,您可以添加:

User

处理完 enum CodingKeys: CodingKey { case name case gender case avatar case keys case items case vip case themeColor } 后,您现在可以在 CodingKey 中实现所需的方法:

User

请注意,这确实意味着您在添加属性时必须维护您的 public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self,forKey: .name) self.gender = try container.decode(Gender.self,forKey: .gender) self.avatar = try container.decode(Data.self,forKey: .avatar) self.keys = try container.decode(Int.self,forKey: .keys) self.items = try container.decode([Item].self,forKey: .items) self.vip = try container.decode(Bool.self,forKey: .vip) self.themeColorSetting = try container.decode(ThemeColor?.self,forKey: .themeColor) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name,forKey: .name) try container.encode(self.gender,forKey: .gender) try container.encode(self.avatar,forKey: .avatar) try container.encode(self.keys,forKey: .keys) try container.encode(self.items,forKey: .items) try container.encode(self.vip,forKey: .vip) try container.encode(self.themeColorSetting,forKey: .themeColor) } 和这些方法。但是,它为您做了一些您无法从自动综合实现中获得的事情。

对于初学者来说,如果您发现无法解码先前编码的 CodingKeys,您可以在 Xcode 中的 User 上启用断点。调试器将在无法解码的特定属性上停止。您无法通过 Swift 的综合 Swift.Error 实现实现这一点。

您自己显式实现 Codable 的另一件事是处理添加字段的可能性,这些字段可能在以前编码的实例中丢失。例如,假设您添加了一个 Codable 属性,但您希望能够解码没有该属性的现有 isModerator: Bool 实例。没问题。

首先将 User 属性本身添加到 isModerator

User

然后您更新您的 var isModerator: Bool

CodingKeys

然后更新enum CodingKeys: CodingKey { case name case gender case avatar case keys case items case vip case themeColor case isModerator // <- Added this }

encode(to: Encoder) throws

最后,在public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name,forKey: .themeColor) try container.encode(self.isModerator,forKey: .isModerator) // <- Added this }

init(from: Decoder) throws

注意:如果您向使用合成实现编码的类添加属性并且现在提供您自己的版本,则您为 {{1} 提供的 public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self,forKey: .themeColor) // Add the following line,using nil coalescing to provide a default value // for old serialized Users that didn't have this property. self.isModerator = try container.decodeIfPresent(Bool.self,forKey: .isModerator) ?? false } 情况}} 必须与之前的属性名称完全匹配,尽管 Swift 确实提供了一些从 JSON 风格的“蛇形案例”到 Swift 风格的“骆驼案例”的转换。您还可以尝试使 enum 扩展符合 CodingKeys,并使用 String 文字代替您的编码密钥。如果您想查看正在使用哪些键,请将编码器中的 CodingKey 转换为 String 并打印出来。不幸的是,默认情况下,使用 Data 生成的 String 转换为字符串会失败,因为它的默认输出格式是二进制的。在这种情况下,将其保存到一个文件中,然后在 Plist 编辑器中打开它以查看正在使用的键。

提供您自己的 Data 实现还意味着您不必对每个属性进行编码。例如,您可能在 PropertyListEncoder 上有一些临时属性 - 它仅在会话期间有意义,不需要存储。由于您已经明确地对您的属性进行了编码,并且您不想对该瞬态属性进行编码,因此在 Codable 中没有什么可做的。您可以在所有初始化器中初始化它,包括 User,就像在任何初始化器中一样...或者在声明点初始化,您甚至不必接触初始化器。