Добавить переменные в уже сохраненную переменную User Defaults

Я скачал быстрый файл, который должен помочь мне с сохранением и загрузкой пользовательских переменных:

import Foundation

protocol ObjectSavable {
func setToObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable
func getToObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable
}

extension UserDefaults: ObjectSavable {
func setToObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable {
    let encoder = JSONEncoder()
    do {
        let data = try encoder.encode(object)
        set(data, forKey: forKey)
    } catch {
        throw ObjectSavableError.unableToEncode
    }
}

func getToObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
    guard let data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
    let decoder = JSONDecoder()
    do {
        let object = try decoder.decode(type, from: data)
        return object
    } catch {
        throw ObjectSavableError.unableToDecode
    }
}
}

enum ObjectSavableError: String, LocalizedError {
case unableToEncode = "Unable to encode object into data"
case noValue = "No data object found for the given key"
case unableToDecode = "Unable to decode object into given type"
}

И у меня есть эта структура Person:

struct Person: Encodable, Decodable {
    var firstName: String
    var lastName: String
    var birthday: Date

    init() {
        self.firstName = "Tim"
        self.lastName = "Cook"
        self.birthday = Date()
    }
}

И у меня также есть этот код для сохранения/загрузки структуры Person (то есть с использованием кода выше)

Сохранение:

print("Saving object...")
    let person: Person = Person()
    
    do {
        try UserDefaults.standard.setToObject(person, forKey: "person")
        print("Object saved successfully")
    } catch let err {
        print("Error while saving object:\n\(err.localizedDescription)")
    }

Загрузка:

print("Loading object...")
    
    do {
        self.person = try UserDefaults.standard.getToObject(forKey: "person", castTo: Person.self)
        print("Successfully load object:\n\(self.person!)")
    } catch let err {
        print("Error while loading object:\n\(err.localizedDescription)")
    }

Теперь все это действительно работает. Но, допустим, я выпускаю свое приложение таким образом, а затем хочу добавить новую переменную в Person, например, я добавлю favorite:

struct Person: Encodable, Decodable {
    var firstName: String
    var lastName: String
    var birthday: Date
    var favorite: Bool = false

    init() {
        self.firstName = "Tim"
        self.lastName = "Cook"
        self.birthday = Date()
    }
}

До обновления приложение (без переменной favorite в Person) сохранялось без переменной favorite. А после обновления приложение попытается загрузить предыдущее сохраненное Person с переменной favorite. И именно здесь он терпит неудачу, потому что в данных более старой версии нет переменной favorite. Так что выдает ошибку.

И мой вопрос: есть ли способ, которым при декодировании Person из пользовательских настроек по умолчанию, если он не находит подходящей переменной (например: favorite), вместо того, чтобы выдавать ошибку, он пытался создать ее автоматически? (от var favorite = false)?

Мой проект: https://github.com/orihpt/Encodable

Заранее спасибо.


person אורי orihpt    schedule 09.11.2020    source источник


Ответы (3)


Один из способов сделать это — добавить собственный код декодирования в Person:

enum CodingKeys : CodingKey {
    case firstName
    case lastName
    case birthday
    case favorite
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    firstName = try container.decode(String.self, forKey: .firstName)
    lastName = try container.decode(String.self, forKey: .lastName)
    birthday = try container.decode(Date.self, forKey: .birthday)
    favorite = try container.decodeIfPresent(Bool.self, forKey: .favorite) ?? false
}

Обратите внимание, что для favorite я использовал decodeIfPresent и по умолчанию false.


Другой способ — объявить favorite необязательным:

var favorite: Bool?

Это приведет к тому, что favorite будет установлено на nil, если оно не существует в данных, а не на false, которое вы хотите. Если вам действительно нужен false, вы можете использовать неявно развернутый необязательный Bool?, и вам нужно будет менять nil на false каждый раз при декодировании:

self.person = try UserDefaults.standard.getToObject(forKey: "person", castTo: Person.self)
if self.person.favorite == nil {
    self.person.favorite = false
}

Если вы боитесь, что можете забыть это сделать, вы можете сделать так, чтобы getToObject принимал только объекты, соответствующие этому протоколу:

protocol HasDefaults {
    func changeNilsToDefaults()
}


extension UserDefaults {
    func getToObject<Object: HasDefaults>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
        guard let data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
        let decoder = JSONDecoder()
        do {
            let object = try decoder.decode(type, from: data)
            object.changeNilsToDefaults() // notice this line!
            return object
        } catch {
            throw ObjectSavableError.unableToDecode
        }
    }
}

И вы не сможете выполнить getToObject(forKey: "person", castTo: Person.self), если не согласуете Person с HasDefaults:

extension Person : HasDefaults {
    func changeNilsToDefaults() {
        if self.person.favorite == nil {
            self.person.favorite = false
        }
    }
}
person Sweeper    schedule 09.11.2020

Предложения @Sweeper являются рабочими решениями. Вы также можете объединить его необязательный подход с решением DTO.

DTO: объект передачи данных

Сохраните свои объекты как объекты DTO и добавьте новые свойства как необязательные после первого выпуска:

struct PersonDTO: Codable {
    let firstName: String
    let lastName: String
    let birthday: Date
    let favorite: Bool?
}

После получения объекта DTO из UserDefaults инициализируйте с его помощью свой объект Person.

struct Person {
    var firstName: String
    var lastName: String
    var birthday: Date
    var favorite: Bool

    init(_ dto: PersonDTO) {
        self.firstName = dto.firstName
        self.lastName = dto.lastName
        self.birthday = dto.birthday
        self.favorite = dto.favorite ?? false
    }
}
person Buğra Cansın Göz    schedule 09.11.2020

Создайте переменную protected:

struct Person: Encodable, Decodable {
    var firstName: String
    var lastName: String
    var birthday: Date
    var favorite: Bool {
        get {
            return favoriteProtected ?? false
        }
        set {
            favoriteProtected = newValue
        }
    }

    private var favoriteProtected: Bool? = nil

    init() {
        self.firstName = "Tim"
        self.lastName = "Cook"
        self.birthday = Date()
    }
}

Таким образом, вам не нужно реализовывать init(from decoder: Decoder), что может занять много времени, если ваш struct большой.

person אורי orihpt    schedule 17.02.2021