Warning: file_get_contents(/data/phpspider/zhask/data//catemap/8/swift/18.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
iOS Swift-在更改持久化数据模型中的属性后读取该模型_Swift_Xcode_Types_Persistence_Decoder - Fatal编程技术网

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

iOS Swift-在更改持久化数据模型中的属性后读取该模型,swift,xcode,types,persistence,decoder,Swift,Xcode,Types,Persistence,Decoder,提前感谢你的帮助 我想要持久化数据,比如用户的统计数据。假设我有一个数据模型,一个带有一些属性的“Stats”类,它被保存到用户的设备上。假设我已经发布了这个应用程序,用户正在记录他们的统计数据,但随后我想在发布新的构建版本之前对类进行更改——更多或更少的属性,甚至可能重命名它们(等等)。但在进行了这些更改之后,“Stats”类型现在与用户在设备上保存的类型不同,因此无法解码,并且在该点之前,用户以前的所有数据似乎都将丢失/无法获取 我如何才能添加这些类型的更改,使PropertyListDec

提前感谢你的帮助

我想要持久化数据,比如用户的统计数据。假设我有一个数据模型,一个带有一些属性的“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]太大,无法保持用户默认值,但即使如此,我认为这个问题仍然存在。。。在网上找不到关于它的任何信息;似乎一旦你让你的用户使用一个持久化的类,你就不能改变它。我尝试为新属性使用默认值,但结果是一样的

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

谢谢

删除属性非常简单。只需从Stat类中删除它的定义,当您再次读取和保存stats时,该属性的现有数据将被删除

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

var newProperty: Int?
第一次解码以前存在的stat时,此属性将为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
}
删除属性非常简单。只需从Stat类中删除它的定义,当您再次读取和保存stats时,该属性的现有数据将被删除

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

var newProperty: Int?
第一次解码以前存在的stat时,此属性将为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
}

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


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

struct StatData: Codable {

    let key: String
    let timesAnsweredCorrectly: Int?
    let timesAnsweredFirstTime: Int?
    let timesFailed: Int?
}
这样,我就可以从文件中读取属性,从结构中添加或删除的任何属性都将返回nil,而不是使整个结构无法读取。然后,我有两个函数将“StatData”转换为“Stat”(和back),在任何函数返回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模型是一个结构,memberwise初始值设定项将警告任何更改数据模型的开发人员,在从文件中读取旧数据时,他们也需要考虑更改


这似乎很管用。如果您有任何意见或其他建议,我们将不胜感激。

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


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

struct StatData: Codable {

    let key: String
    let timesAnsweredCorrectly: Int?
    let timesAnsweredFirstTime: Int?
    let timesFailed: Int?
}
这样,我就可以从文件中读取属性,从结构中添加或删除的任何属性都将返回nil,而不是使整个结构无法读取。然后我有两个函数将“StatData”转换为“Stat”(和back),p