【问题标题】:iOS Swift - reading a persisted data model after having altered properties in that modeliOS Swift - 在该模型中更改属性后读取持久数据模型
【发布时间】:2020-08-01 04:24:42
【问题描述】:

提前感谢您的帮助。

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

如何以 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() 将返回 nil。

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

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

谢谢!!

【问题讨论】:

    标签: swift xcode types persistence decoder


    【解决方案1】:

    移除一个属性很简单。只需从 Stat 类中删除其定义,当您再次读取和保存统计信息时,该属性的现有数据将被删除。

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

    var newProperty: Int?
    

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

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

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

    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
    }
    

    【讨论】:

    • 谢谢你,我不确定我是否错过了什么。我的方法完全遵循了 Apple 自己的指导方针,但他们没有提到任何关于编码类型是否在构建之间发生变化的问题——这似乎是数据持久性的一个非常重要的考虑因素。将玩转可选属性和您的迁移方案想法。
    【解决方案2】:

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


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

    struct StatData: Codable {
    
        let key: String
        let timesAnsweredCorrectly: Int?
        let timesAnsweredFirstTime: Int?
        let timesFailed: Int?
    }
    

    这样我可以从文件中读取属性,并且从结构中添加或删除的任何属性都只会返回 nil 而不是使整个结构不可读。然后我有两个函数将“StatData”转换为“Stat”(并返回),提供默认值以防万一返回 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 类中选择要保存的属性,并且由于 StatData 模型是一个结构体,因此成员初始化程序将警告任何更改数据模型的开发人员,他们也需要考虑从文件中读取旧数据时的变化。

    这似乎完成了这项工作。任何 cmets 或其他建议将不胜感激

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-01
      • 1970-01-01
      • 2014-09-27
      • 2012-12-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多