iOS Swift - чтение сохраненной модели данных после изменения свойств в этой модели - PullRequest
1 голос
/ 18 апреля 2020

Заранее спасибо за помощь.

Я хочу сохранить данные, такие как статистика пользователя. Допустим, у меня есть модель данных, класс 'Stats' с несколькими свойствами, и он сохраняется на устройстве пользователя. Предположим, что я выпустил приложение, пользователи записывают свою статистику, но позже я хочу внести изменения в класс - больше или меньше свойств, возможно, даже переименовать их (и т. Д. c.), До выхода новой версии сборки. , Но после внесения этих изменений тип «Статистика» теперь отличается от того, который пользователи сохранили на своем устройстве, поэтому он не сможет декодировать, и похоже, что все предыдущие данные пользователя до этого момента будут быть потерянным / недостижимым.

Как добавить такие изменения в класс таким образом, чтобы 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 () вернет ноль.

Любой совет будет отличным! Я уверен, что я поступаю об этом неправильно. Я полагал, что массив [Stat] будет слишком большим, чтобы его можно было сохранить в UserDefaults, но даже тогда я думаю, что эта проблема все еще существует ... Не могу найти что-нибудь об этом в Интернете; кажется, что, когда ваши пользователи используют постоянный класс, вы не можете его изменить. Я попытался использовать значения по умолчанию для новых свойств, но результат тот же.

Единственное решение, которое я могу придумать, - это разбить класс на литералы и вместо этого сохранить все эти в виде кортежа / словаря. Затем я декодировал бы эти необработанные данные и имел бы функцию для сборки и создания класса из любых соответствующих данных, которые все еще могут быть взяты из старой версии типа «Stat». Похоже, большой обходной путь, и я уверен, что вы, ребята, знаете гораздо лучше.

Спасибо !!

Ответы [ 2 ]

0 голосов
/ 21 апреля 2020

Исходя из ответа Майка, я придумал схему миграции, которая, похоже, решает проблему дополнительных функций и не требует никаких новых классов при каждом изменении модели данных. Частично проблема заключается в том, что разработчик может изменить или добавить свойства в класс, который сохраняется, и XCode никогда не будет отмечать это как проблему, что может привести к тому, что приложение вашего пользователя попытается прочитать предыдущий класс данных, сохраненный на устройстве, возвращая ноль. и, по всей вероятности, перезаписывая все данные с помощью переформатированной модели.


Вместо записи класса (например, Stat) на диск (что Apple предлагает в своих учебных материалах), я сохраняю новая структура "StatData", которая содержит только необязательные свойства данных, которые я хочу записать в файл:

struct StatData: Codable {

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

Таким образом, я могу читать свойства из файла и любые добавленные или удаленные свойства из struct просто вернет nil вместо того, чтобы сделать всю структуру нечитаемой. Затем у меня есть две функции для преобразования «StatData» в «Stat» (и обратно), предоставляя значения по умолчанию в случае, если какие-либо были возвращены ноль.

    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 является структурой, инициализатор по элементам предупредит любого разработчика, который изменяет модель данных, которую они также должны будут учитывать изменение при чтении старых данных из файла.

Это похоже на работу. Любые комментарии или другие предложения будут оценены

0 голосов
/ 18 апреля 2020

Удалить свойство достаточно просто. Просто удалите его определение из класса 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
}
...