I have downloaded a swift file that should help me with saving and loading custom variables:
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"
}
And I have this Person
struct:
struct Person: Encodable, Decodable {
var firstName: String
var lastName: String
var birthday: Date
init() {
self.firstName = "Tim"
self.lastName = "Cook"
self.birthday = Date()
}
}
And I also have this code for saving/loading a Person
struct (that is using the code from above)
Saving:
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)")
}
Loading:
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)")
}
Now, all of this does work.
But, let's say that I release my app that way, and then I want to add a new variable to Person
, for example, I'll add a 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()
}
}
Before the update, the app (without the favorite
variable in Person
) would save without the favorite
variable. And after the update, the app will try to load the previous saved Person
with the favorite
variable. And that is where it failes, because the older-version data doesn't have a favorite
variable in it. So it throws an error.
And my question is, is there a way that when it decoding a Person
from User Defaults, if it doesn't find any matching variable (for example: favorite
), instead of throwing an error, it will try to auto-create it? (from var favorite
= false
)?
My project: https://github.com/orihpt/Encodable
Thanks in advance.
One way to do this is to add custom decoding code into 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
}
Note that for favorite
, I used decodeIfPresent
and defaults to false
.
Another way is to declare favorite
as optional:
var favorite: Bool?
This will cause favorite
to be set to nil
if it doesn't exist in the data, not the false
that you want. If you really want false
, you can use an implicitly unwrapped optional Bool?
, and you'd need to change the nil
to false
every time you decode:
self.person = try UserDefaults.standard.getToObject(forKey: "person", castTo: Person.self)
if self.person.favorite == nil {
self.person.favorite = false
}
If you are afraid that you might forget to do this, you can make getToObject
only accept objects that conform to this protocol:
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
}
}
}
And you won't be able to do getToObject(forKey: "person", castTo: Person.self)
unless you conform Person
to HasDefaults
:
extension Person : HasDefaults {
func changeNilsToDefaults() {
if self.person.favorite == nil {
self.person.favorite = false
}
}
}