Home > Enterprise >  Fatal error when publishing UserDefaults with Combine
Fatal error when publishing UserDefaults with Combine

Time:08-18

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
  • Related