在 Swift 中实现数据存储的最佳方法是啥,如何处理类更改

Posted

技术标签:

【中文标题】在 Swift 中实现数据存储的最佳方法是啥,如何处理类更改【英文标题】:What's the best way to implement data storage in Swift, how to deal with class change在 Swift 中实现数据存储的最佳方法是什么,如何处理类更改 【发布时间】:2021-06-11 18:31:47 【问题描述】:

我是这里的一个新的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() 一起工作正常

【问题讨论】:

添加新方法应该不是问题。你还改变了什么? 发现每次换class时,老版本的存储数据都无法读取 你能显示minimal reproducible example吗? 正如@Sweeper 指出的那样,添加方法不应改变Codable 解码您的User 对象的能力。但是,添加存储的属性会影响它。您的代码没有显示任何属性更改,但您遇到的困难表明您可能这样做了 - 可能是在其中一种属性类型中(如 ItemGender)。 您将新属性设为可选,如果未设置则提供默认值。 【参考方案1】:

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

跟踪它的一种方法,并且还为将来可能对User 的存储属性所做的更改提供一些向后兼容性,是在User 中完全实现Codable 协议,而不是依赖于一个 Swift 为你合成。

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

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

Encoders 会将您的类序列化为“对象”类型,它本质上是一个字典。因此,您需要一个类型作为该字典的键,并且它需要符合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 和这些方法。但是,它为您做了一些您无法从自动合成的实现中获得的东西。

对于初学者,如果您发现无法解码以前编码的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

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

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

【讨论】:

我想我确实改了其他相关类的属性,但是我忘记了,后来我通过在用户类中添加一个方法进行测试,添加后可以读取编码数据谢谢说了这么多,对我有用,数据存储部分还需要多学习,谢谢大家的努力

以上是关于在 Swift 中实现数据存储的最佳方法是啥,如何处理类更改的主要内容,如果未能解决你的问题,请参考以下文章

在 Python 中实现 char const *const names[] 的最佳方法是啥

在 QT 中实现具有多个小部件的视图的最佳方法是啥?

在 SwiftUI 中实现昂贵的派生属性的最佳方法是啥?

在 ViewPager 中实现弹性/反弹动画效果的最佳方法是啥?

在 C++ 中实现断言检查的最佳方法是啥?

在 Android 中实现与多个设备的蓝牙连接的最佳方法是啥?