问题描述
我是这里的一个新的swift程序员,正在用自己的项目实践,在实现数据存储的时候发现有很多方法可以做到,比如UserDefaults、Plist、CoreData…… 我选择了 Plist 作为我自己的数据持久化方法,我立即发现有一个问题,要存储那些自定义类,我需要使其遵循 Codable 协议。
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
添加一个方法不应改变 PropertyListDecoder
或 JSONDecoder
对其进行解码的能力。但是,如果您确实添加了存储属性,或如果您更改了其他 Codable
属性,例如 Item
或 Gender
,您很可能无法从这些类型的先前版本编码的数据中对其进行解码。
跟踪它并为将来可能对 User
的存储属性进行的更改提供一些向后兼容性的一种方法是在 {{1} 中完全实现 Codable
协议而不是依赖 Swift 为您合成的一个。
User
基本上只是另外两个协议 Codable
和 Encodable
的组合。
Decodable
需要一个函数 Encodable
。 encode(to: Encoder) throws
需要一个初始化程序,Decodable
。
init(from: Decoder) throws
s 将您的类序列化为“对象”类型,它本质上是一个字典。因此,您需要一个类型作为该字典的键,并且它需要符合 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
,就像在任何初始化器中一样...或者在声明点初始化,您甚至不必接触初始化器。