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?