I need to implement my own version of @AppStorage, as I also want to sync the user defaults to other devices, via iCloud's KeyValue storage.
I need the projectedValue to return a binding so I can use it like this:
struct ContentView: View {
@SyncedAppStorage(key: "testKey", defaultValue: true) private var test
var body: some View {
Toggle(isOn: $test) {
Text("Test")
}
}
}
In my first attempt the binding property implementation complained I am mutating self:
@propertyWrapper struct SyncedAppStorage<Value>: DynamicProperty {
let key: String
var cachedValue: Value
init(key: String, defaultValue: Value) {
self.key = key
self.cachedValue = UserDefaults.standard.object(forKey: key) as? Value) ?? defaultValue
}
var wrappedValue: Value {
get { getValue() }
set { setNewValue(newValue) }
}
var projectedValue: Binding<Value> {
.init(get: getValue, set: { n in setNewValue(n)} ) // <-- Compile error ...self is immutable
}
private func getValue() -> Value { cachedValue }
private mutating func setNewValue(_ newValue: Value) {
UserDefaults.standard.set(newValue, forKey: key)
NSUbiquitousKeyValueStore.default.set(newValue, forKey: key)
cachedValue = newValue
}
}
My next attempt uses an @State property in the property wrapper - which works...
@propertyWrapper struct SyncedAppStorage <Value>: DynamicProperty {
let key: String
@State var cachedValue: Value
init(key: String, defaultValue: Value) {
self.key = key
self._cachedValue = State(initialValue: (UserDefaults.standard.object(forKey: key) as? Value) ?? defaultValue)
}
var wrappedValue: Value {
get { getValue() }
set { setNewValue(newValue) }
}
var projectedValue: Binding<Value> {
.init(get: getValue, set: { n in setNewValue(n)} )
}
private func getValue() -> Value { cachedValue }
private func setNewValue(_ newValue: Value) {
UserDefaults.standard.set(newValue, forKey: key)
cachedValue = newValue
NSUbiquitousKeyValueStore.default.set(newValue, forKey: key)
}
}
But is the right way - are there better solutions?
You can use @State here if you want. But out of curiosity I came up with another solution, that uses Combine to work. The issue with your first implementation is the implicit self capturing in your Binding.
@propertyWrapper struct SyncedAppStorage<Value> {
let key: String
private var sinks = Set<AnyCancellable>()
// subject definition
let subject: CurrentValueSubject<Value,Never>
init(key: String, defaultValue: Value) {
self.key = key
//Initialize the subject
subject = CurrentValueSubject(UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue)
//observe changes to subject values
subject.map{
UserDefaults.standard.set($0, forKey: key)
NSUbiquitousKeyValueStore.default.set($0, forKey: key)
}.sink(receiveValue: {_ in})
.store(in: &sinks)
}
var wrappedValue: Value{
get{ subject.value }
set{ subject.value = newValue}
}
var projectedValue: Binding<Value> {
return .init(get: { wrappedValue}, set: { subject.value = $0 })
}
}
I removed your cachedValue implementation, as (within the context you provided) i don´t think it is needed.
Kudos to Louis Lac for the link.