iOS Swift - 在更改模型中的属性后读取持久数据模型

iOS Swift - reading a persisted data model after having altered properties in that model

在此先感谢您的帮助。

我想保留用户统计数据等数据。假设我有一个数据模型,一个具有一些属性的 class 'Stats',它被保存到用户的设备上。假设我已经发布了该应用程序,用户正在记录他们的统计数据,但稍后我想更改 class - 更多或更少的属性,甚至可能在新构建之前重命名它们(等)发布。但在进行这些更改后,'Stats' 类型现在与用户保存在其设备上的类型不同,因此它将无法解码,并且似乎用户之前的所有数据都在此之前点将是 lost/unattainable.

我如何添加对 class 进行这些类型的更改,使 PropertyListDecoder 仍然能够解码仍在用户设备上的统计信息?

这基本上就是我所拥有的:

class Stat: Codable  {

    let questionCategory = questionCategory()

    var timesAnsweredCorrectly: Int = 0
    var timesAnsweredFirstTime: Int = 0
    var timesFailed: Int = 0

    static func saveToFile(stats: [Stat]) {

        let propertyListEncoder = PropertyListEncoder()
        let encodedSettings = try? propertyListEncoder.encode(stats)
        try? encodedSettings?.write(to: archiveURL, options: .noFileProtection)
    }

    static func loadFromFile() -> [Stat]? {
        let propertyListDecoder = PropertyListDecoder()
        if let retrievedSettingsData = try? Data(contentsOf: archiveURL), let decodedSettings = try? propertyListDecoder.decode([Stat].self, from: retrievedSettingsData) {

            return decodedSettings
        } else {
            return nil
        }
    }
}

static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

static let archiveURL = documentsDirectory.appendingPathComponent("savedVerbStats").appendingPathExtension("plist")

似乎即使只是向 'Stat' 添加一个新的 属性 也会导致用户之前的持久化数据变得不可解码为 'Stat' 类型,并且 loadFromFile() 将 return无。

任何建议都很好!我确定我会以错误的方式解决这个问题。我认为数组 [Stat] 太大而无法在 UserDefaults 中持续存在,但即便如此我认为这个问题仍然存在......在网上找不到任何相关信息;似乎一旦你让你的用户使用了一个持久的 class 你就不能再改变它了。我尝试为新属性使用默认值,但结果是一样的。

我能想到的唯一解决方案是将 class 分解为文字,然后将所有 这些 保存为某种 tuple/dictionary 形式。然后我会解码原始数据,并有一个函数 assemble 并从仍然可以从旧版本的 'Stat' 类型中获取的任何相关数据创建 class 。似乎是一个很大的解决方法,我相信你们知道更好的方法。

谢谢!!

删除一个属性很容易。只需从 Stat class 中删除它的定义,当您再次读取和保存统计数据时,该 属性 的现有数据将被删除。

添加 新属性的关键是使它们成为可选属性。例如:

var newProperty: Int?

当第一次解码先前存在的统计数据时,此 属性 将为零,但所有其他属性都将正确设置。您可以根据需要设置并保存新的属性。

将所有新属性设为可选可能会带来一些不便,但它为其他可能的迁移方案打开了大门,而且不会丢失数据。

编辑:这是一个更复杂的迁移方案,它避免了新属性的可选。

class Stat: Codable {
    var timesAnsweredCorrectly: Int = 0
    var timesAnsweredFirstTime: Int = 0
    var timesFailed: Int = 0

    //save all stats in the new Stat2 format
    static func saveToFile(stats: [Stat2]) {
        let propertyListEncoder = PropertyListEncoder()
        let encodedSettings = try? propertyListEncoder.encode(stats)
        try? encodedSettings?.write(to: archiveURL, options: .noFileProtection)
    }

    //return all stats in the new Stat2 format
    static func loadFromFile() -> [Stat2]? {
        let propertyListDecoder = PropertyListDecoder()
        //first, try to decode existing stats as Stat2
        if let retrievedSettingsData = try? Data(contentsOf: archiveURL), let decodedSettings = try? propertyListDecoder.decode([Stat2].self, from: retrievedSettingsData) {

            return decodedSettings
        } else if let retrievedSettingsData = try? Data(contentsOf: archiveURL), let decodedSettings = try? propertyListDecoder.decode([Stat].self, from: retrievedSettingsData) {
            //since we couldn't decode as Stat2, we decoded as Stat

            //convert existing Stat instances to Stat2, giving the newProperty an initial value
            var newStats = [Stat2]()
            for stat in decodedSettings {
                let newStat = Stat2()
                newStat.timesAnsweredCorrectly = stat.timesAnsweredCorrectly
                newStat.timesAnsweredFirstTime = stat.timesAnsweredFirstTime
                newStat.timesFailed = stat.timesFailed
                newStat.newProperty = 0
                newStats.append(newStat)
            }
            return newStats
        } else {
            return nil
        }
    }
    static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

    static let archiveURL = documentsDirectory.appendingPathComponent("savedVerbStats").appendingPathExtension("plist")
}

class Stat2: Stat {
    var newProperty: Int = 0
}

根据 Mike 的回复,我想出了一个迁移方案,该方案似乎可以解决可选问题,并且每次更改数据模型时都不需要任何新的 classes。问题的一部分是开发人员可能会更改或添加属性到 class 并且 Xcode 永远不会将此标记为问题,这可能会导致您的用户的应用程序尝试读取以前的数据class 已保存到设备,return 为零,并且很可能会用重新格式化的模型覆盖所有相关数据。


我没有将 class(例如 Stat)写入磁盘(这是 Apple 在其教学资源中的建议),而是保存了一个新结构 "StatData",它仅包含我要写入文件的数据:

struct StatData: Codable {

    let key: String
    let timesAnsweredCorrectly: Int?
    let timesAnsweredFirstTime: Int?
    let timesFailed: Int?
}

这样我就可以从文件中读取属性,并且从结构中添加或删除的任何属性都只会 return nil 而不是使整个结构不可读。然后我有两个函数将 'StatData' 转换为 'Stat'(并返回),提供默认值以防任何 returned nil。

    static func convertToData(_ stats: [Stat]) -> [StatData] {

        var data = [StatData]()
        for stat in stats {
            let dataItem = StatData(key: stat.key, timesAnsweredCorrectly: stat.timesAnsweredCorrectly, timesAnsweredFirstTime: stat.timesAnsweredFirstTime, timesFailed: stat.timesFailed)
            data.append(dataItem)
        }
        return data
    }

    static func convertFromData(_ statsData: [StatData]) -> [Stat] {

        // if any of these properties weren't previously saved to the device, they will return the default values but the rest of the data will remain accessible.
        var stats = [Stat]()
        for item in statsData {
            let stat = stat.init(key: item.key, timesAnsweredCorrectly: item.timesAnsweredCorrectly ?? 0, timesAnsweredFirstTime: item.timesAnsweredFirstTime ?? 0, timesFailed: item.timesFailed ?? 0)
            stats.append(stat)
        }
        return stats
    }

然后我在读取数据或将数据保存到磁盘时调用这些函数。这样做的好处是我可以从 Stat class 中选择我想要保存的属性,并且因为 StatData 模型是一个结构,成员初始化器将警告任何更改数据模型的开发人员他们也将从文件中读取旧数据时需要考虑更改。

这似乎可以解决问题。如有任何意见或其他建议,我们将不胜感激