在 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
对象的能力。但是,添加存储的属性会影响它。您的代码没有显示任何属性更改,但您遇到的困难表明您可能这样做了 - 可能是在其中一种属性类型中(如 Item
或 Gender
)。
您将新属性设为可选,如果未设置则提供默认值。
【参考方案1】:
虽然您的代码和描述表明您只添加了方法,而不是属性,但简单地向User
添加方法不应改变PropertyListDecoder
或JSONDecoder
对其进行解码的能力。但是,如果您确实添加了一个存储属性,或如果您更改了其他 Codable
属性,例如 Item
或 Gender
,您很可能无法从是从这些类型的先前版本编码的。
跟踪它的一种方法,并且还为将来可能对User
的存储属性所做的更改提供一些向后兼容性,是在User
中完全实现Codable
协议,而不是依赖于一个 Swift 为你合成。
Codable
基本上只是两个其他协议的组合,Encodable
和 Decodable
。
Encodable
需要一个函数,encode(to: Encoder) throws
。 Decodable
需要一个初始化器,init(from: Decoder) throws
。
Encoder
s 会将您的类序列化为“对象”类型,它本质上是一个字典。因此,您需要一个类型作为该字典的键,并且它需要符合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[] 的最佳方法是啥