Necesito implementar mi propia versión de @AppStorage, ya que también quiero sincronizar los valores predeterminados del usuario con otros dispositivos, a través del almacenamiento KeyValue de iCloud.
Necesito el valor proyectado para devolver un enlace para poder usarlo así:
struct ContentView: View { @SyncedAppStorage(key: "testKey", defaultValue: true) private var test var body: some View { Toggle(isOn: $test) { Text("Test") } } }En mi primer intento, la implementación de la propiedad vinculante se quejó de que me estaba mutando a mí mismo:
@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 } }Mi próximo intento utiliza una propiedad @State en el contenedor de propiedades, que funciona...
@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) } }Pero, ¿es el camino correcto? ¿Hay mejores soluciones?
Puede usar @State aquí si lo desea. Pero por curiosidad, se me ocurrió otra solución, que usa Combine para funcionar. El problema con su primera implementación es la autocaptura implícita en su enlace.
@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 }) } }Eliminé su implementación de cachedValue, ya que (dentro del contexto que proporcionó) no creo que sea necesario.
Felicitaciones a Louis Lac por el enlace.