Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Store custom data type in @AppStorage with optional initializer?

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)
    }
    
}
like image 443
nicksarno Avatar asked Jan 05 '21 23:01

nicksarno


2 Answers

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.

like image 82
pawello2222 Avatar answered Oct 16 '22 18:10

pawello2222


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),
              let json = String(data: data, encoding: .utf8)
        else {
            return "{}"
        }
        return json
    }

    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let value = try? JSONDecoder().decode(Self.self, from: data)
        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
like image 2
markiv Avatar answered Oct 16 '22 18:10

markiv