Reputation: 4245
I'm trying to store a custom data type into AppStorage
. To do so, the model conforms to RawRepresentable
(followed this tutorial). It's working fine, but when I initialize the @AppStorage
variable, it requires an initial UserModel
value. I want to make the variable optional, so it can be nil if the user is signed out. Is this possible?
Within a class / view, I can init like this:
@AppStorage("user_model") private(set) var user: UserModel = UserModel(id: "", name: "", email: "")
But I want to init like this:
@AppStorage("user_model") private(set) var user: UserModel?
Model:
struct UserModel: Codable {
let id: String
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case id
case name
case email
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try String(values.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try String(values.decode(String.self, forKey: .id))
}
self.name = try values.decode(String.self, forKey: .name)
self.email = try values.decode(String.self, forKey: .email)
}
init(id: String, name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
}
// MARK: RAW REPRESENTABLE
extension UserModel: RawRepresentable {
// RawRepresentable allows a UserModel to be store in AppStorage directly.
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(UserModel.self, from: data)
else {
return nil
}
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(email, forKey: .email)
}
}
Upvotes: 12
Views: 5210
Reputation: 299345
Both of the answers here are somewhat dangerous. They conform a type the author does not control (Optional) to a protocol (RawRepresentable) that the author does not control. This is a cause of very difficult to solve bugs. There can only be one conformance of a type to a protocol in the whole system. If any other part of the system (for example, a package you import or something internal to Apple) also creates this conformance, the system will no longer build, or worse, could lead to undefined behavior that the compiler won't catch. See Redundant Conformance for an example of how this fails in confusing ways.
First, note that AppStorage (which is just UserDefaults) is not meant to store large, complex data. It is generally meant to be a convenient plact to store relatively small bits of data. Even so, it is helpful to be able to store custom types there. A general solution is to split the property from its storage:
@AppStorage("user_model") private var userStorage: Data = Data()
private(set) var user: UserModel {
get {
(try? JSONDecoder().decode(UserModel.self, from: userStorage))
?? UserModel(id: "", name: "", email: "")
}
set { userStorage = (try? JSONEncoder().encode(newValue)) ?? Data() }
}
You may find it useful to create your own protocol to make the code easier to follow:
protocol AppStorable: Codable {
// So there's a default value when nothing is in app storage
init()
}
extension AppStorable {
static func readFromAppStorage(_ data: AppStorage<Data>) -> Self {
(try? JSONDecoder().decode(Self.self, from: data.wrappedValue)) ?? Self.init()
}
static func writeToAppStorage(_ data: AppStorage<Data>, newValue: Self) {
data.wrappedValue = (try? JSONEncoder().encode(newValue)) ?? Data()
}
}
...
@AppStorage("user_model") private var userStorage: Data = Data()
private(set) var user: UserModel {
get { UserModel.readFromAppStorage(_userStorage) }
set { UserModel.writeToAppStorage(_userStorage, newValue: newValue) }
}
This approach doesn't run the risk of collisions with other code trying to do the same thing.
Upvotes: 3
Reputation: 1664
A possible generic approach for any optional Codable
:
extension Optional: RawRepresentable where Wrapped: Codable {
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self) else {
return "{}"
}
return String(decoding: data, as: UTF8.self)
}
public init?(rawValue: String) {
guard let value = try? JSONDecoder().decode(Self.self, from: Data(rawValue.utf8)) else {
return nil
}
self = value
}
}
With that in place, any Codable
can now be persisted in app storage:
@AppStorage("user_model") var user: UserModel? = nil
Upvotes: 12
Reputation: 54516
The code below works because you added a conformance UserModel: RawRepresentable
:
@AppStorage("user_model") private(set) var user: UserModel = UserModel(id: "", name: "", email: "")
You need to do the same for UserModel?
if you want the following to work:
@AppStorage("user_model") private(set) var user: UserModel? = nil
Here is a possible solution:
extension Optional: RawRepresentable where Wrapped == UserModel {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(UserModel.self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserModel.CodingKeys.self)
try container.encode(self?.id, forKey: .id)
try container.encode(self?.name, forKey: .name)
try container.encode(self?.email, forKey: .email)
}
}
Note: I reused the implementation you already had for UserModel: RawRepresentable
- it might need some corrections for this case.
Also because you conform Optional: RawRepresentable
you need to make
UserModel
public as well.
Upvotes: 6