Lucas C. Feijo
Lucas C. Feijo

Reputation: 1131

How to seamlessly wrap @Published variables with a UserDefaults propertyWrapper

I've been looking for the perfect UserDefaults wrapper that

My starting point was this StackOverflow answer, but it uses object:forKey which doesn't work with custom objects (encoding URLs is always non-trivial for me).

My idea is to be able to use it like so:

struct Server: Identifiable, Codable, Equatable, Hashable { /* vars */ }

class ServerPickerViewModel: ObservableObject {

  @Published(wrappedValue: Server.defaultServers.first!,
             type: Server.self,
             key: "currentServer")
  var currentServer: Server?

}

To achieve this, I modified the code from @VictorKushnerov's answer:

import Combine

private var cancellables = [String: AnyCancellable]()

extension Published {
  init<T: Encodable & Decodable>(wrappedValue defaultValue: T, type: T.Type, key: String) {
      let decoder = JSONDecoder()
      var value: T
      if
        let data = UserDefaults.standard.data(forKey: key),
        let decodedVal = try? decoder.decode(T.self, from: data) {
        value = decodedVal
      } else {
        value = defaultValue
      }

        self.init(initialValue: value) // <-- Error

        cancellables[key] = projectedValue.sink { val in
          let encoder = JSONEncoder()
          let encodedVal = encoder.encode(val) // <-- Error
          UserDefaults.standard.set(encodedVal, forKey: key)
        }
    }
}

There are currently two errors I can't get through, which are the following:

Upvotes: 2

Views: 206

Answers (1)

jnpdx
jnpdx

Reputation: 52416

You can fix your compilation errors by using where Value : Codable to restrict your extension. Then, you can get rid of your T generic altogether (& you don't have to use the type argument either):

extension Published where Value : Codable {
  init(wrappedValue defaultValue: Value, key: String) {
      let decoder = JSONDecoder()
      var value: Value
      if
        let data = UserDefaults.standard.data(forKey: key),
        let decodedVal = try? decoder.decode(Value.self, from: data) {
        value = decodedVal
      } else {
        value = defaultValue
      }

        self.init(initialValue: value)

        cancellables[key] = projectedValue.sink { val in
          let encoder = JSONEncoder()
            do {
                let encodedVal = try encoder.encode(val)
                UserDefaults.standard.set(encodedVal, forKey: key)
            } catch {
                print(error)
                assertionFailure(error.localizedDescription)
            }
        }
    }
}

This being said, I'd probably take the path instead of creating a custom property wrapper instead that wraps @AppStorage instead of extending @Published

Upvotes: 1

Related Questions