I try to observe my array of custom objects in my UserDefaults using a Combine
publisher.
First my extension:
extension UserDefaults {
var ratedProducts: [Product] {
get {
guard let data = UserDefaults.standard.data(forKey: "ratedProducts") else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "ratedProducts")
}
}
}
Then in my View model, within my init()
I do:
UserDefaults.standard
.publisher(for: \.ratedProducts)
.sink { ratedProducts in
self.ratedProducts = ratedProducts
}
.store(in: &subscriptions)
You can see that I basically want to update my @Published
property ratedProducts
in the sink call.
Now when I run it, I get:
Fatal error: Could not extract a String from KeyPath Swift.ReferenceWritableKeyPath<__C.NSUserDefaults, Swift.Array<RebuyImageRating.Product>>
I think I know that this is because in my extension the ratedProduct property is not marked as @objc
, but I cant mark it as such because I need to store a custom type.
Anyone know what to do?
Thanks
CodePudding user response:
As you found out you can not observe your custom types directly, but you could add a possibility to observe the data change and decode that data to your custom type in your View model:
extension UserDefaults{
// Make it private(set) so you cannot use this from the outside and set arbitary data by accident
@objc dynamic private(set) var observableRatedProductsData: Data? {
get {
UserDefaults.standard.data(forKey: "ratedProducts")
}
set { UserDefaults.standard.set(newValue, forKey: "ratedProducts") }
}
var ratedProducts: [Product]{
get{
guard let data = UserDefaults.standard.data(forKey: "ratedProducts") else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
} set{
// set the custom objects array through your observable data property.
observableRatedProductsData = try? PropertyListEncoder().encode(newValue)
}
}
}
and the observer in your init
:
UserDefaults.standard.publisher(for: \.observableRatedProductsData)
.map{ data -> [Product] in
// check data and decode it
guard let data = data else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
}
.receive(on: RunLoop.main) // Needed if changes come from background
.assign(to: &$ratedProducts) // assign it directly