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

What's the best way to implement data storage in Swift, how to deal with class change

我是新来的swift程序员,正在用自己的项目实践,在实现数据存储的时候,发现有很多方法可以实现,比如UserDefaults, Plist,核心数据... 我选择了 Plist 作为我自己的数据持久化方法,我立即发现有一个问题,要存储那些自定义 classes,我需要按照 Codable 协议来制作它。

比如我的自定义class用户有变量和函数


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()!
    }

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

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

更新用户导致的崩溃问题class:

我有

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 中有一个存储的默认用户,这一切都发生在我只是简单地向用户添加一个新功能时 Class,如果我删除应用程序并重新安装它,它与调用

的 loadUser() 一起正常工作

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

追踪它并为您将来可能对 User 的存储属性所做的更改提供一些向后兼容性的一种方法是在 [= 中完全实现 Codable 协议16=] 而不是依赖 Swift 为您综合。

Codable 基本上只是其他两个协议的组合,EncodableDecodable

Encodable 需要函数 encode(to: Encoder) throwsDecodable 需要初始化程序,init(from: Decoder) throws

Encoders 会将您的 class 序列化为“对象”类型,它本质上是一个字典。因此,您需要一个类型作为该字典的键,并且它需要符合 CodingKey 协议。它应该为每个要序列化的 属性 指定一个特定值。通常,它被实现为 enum.

对于您的 User class 您可以添加:

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 和这些方法。但是,它会为您做一些您无法从自动合成实现中获得的事情。

对于初学者,如果您发现无法解码先前编码的 User,您可以在 Xcode 中的 Swift.Error 上启用断点。调试器将在无法解码的特定 属性 处停止。 Swift 的合成 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: .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)
    try container.encode(self.isModerator, forKey: .isModerator) // <- Added this
}

最后,在 init(from: Decoder) throws:

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)

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

注意:如果您要将 属性 添加到使用合成实现编码的 class 并且现在提供您自己的版本,您为 CodingKeys 提供的 enum 案例必须与之前的 属性 名称完全匹配,尽管 Swift 确实提供了一些从 JSON 风格的“蛇案例”的转换Swift 风格的“骆驼案”。您也可以尝试使 String 扩展符合 CodingKey,并使用 String 文字代替您的编码密钥。如果您想查看正在使用的键,请将 Data 从编码器转换为 String 并打印出来。不幸的是,使用 PropertyListEncoder 生成的 Data 默认情况下转换为字符串会失败,因为它的默认输出格式是二进制。在这种情况下,将其保存到一个文件中,然后在 Plist 编辑器中打开它以查看正在使用的密钥。

提供您自己的 Codable 实现也意味着您不必对每个 属性 进行编码。例如,您可能在 User 上有一些临时的 属性 - 它只在会话期间有意义,不需要存储。由于您已经显式地编码了您的属性,并且您不想对那个瞬态编码,因此在 encode(to: Encoder) throws 中无事可做。您可以在所有初始化器中初始化它,包括 init(from: Decoder) throws 就像在任何初始化器中一样......或者在声明点初始化,您甚至不必触摸初始化器。