Skip to content

Instantly share code, notes, and snippets.

@Gernot
Created October 16, 2020 16:40
Show Gist options
  • Save Gernot/9d61dff3d7579b7cdaa5ed6760ab502f to your computer and use it in GitHub Desktop.
Save Gernot/9d61dff3d7579b7cdaa5ed6760ab502f to your computer and use it in GitHub Desktop.
Model with Properties form AppStorage
import Foundation
import SwiftUI
import Combine
import PlaygroundSupport
struct Model {
@AppStorage(wrappedValue: true, "Test") var isEnabled
}
struct MyView: View {
@State var model = Model()
@AppStorage(wrappedValue: true, "Test") var valueFromPreferences
var body: some View {
Form {
Toggle("State", isOn: model.$isEnabled)
Toggle("Preferences", isOn: $valueFromPreferences)
}
}
}
PlaygroundPage.current.setLiveView(MyView())
@Gernot
Copy link
Author

Gernot commented Oct 16, 2020

Thanks a lot! This works, but I see a few issues:

  • The model is now a class, and for reasons that are out of scope here, that's not a good option for me. I am looking for extending a struct with a property, without making additional changes to the model.
  • Observing the UserDefaults with a catch-all-notification triggers too often. You debounce it, but it still triggers a lot on unrelated changes. It'd be better to observe the key instead of the whole defaults.
  • ObservableObject already has a default objectWillChange publisher that does not need to be overridden.

I went for a different approach in the meantime:

  • I am still using a property wrapper, but that wraps the original AppStorage.
  • In the View, I have to add the AppStorage as a View property that can trigger the update. This way I get around having to turn the struct into an observableObject class.
  • I am using the projectedValue for getting the AppStorage.
import Foundation
import SwiftUI
import Combine
import PlaygroundSupport

@propertyWrapper
struct WrappedAppStorage<T> {
    
    init(wrappedValue defaultValue: T,
         _ key: String,store: UserDefaults = UserDefaults.standard) where T == Bool {
        self.appStorage = AppStorage<T>(wrappedValue: defaultValue, key, store: store)
    }
    
    let appStorage: AppStorage<T>
    
    var wrappedValue: T {
        get { appStorage.wrappedValue }
        set { appStorage.wrappedValue = newValue }
    }
    
    var projectedValue: AppStorage<T> { appStorage }
    
}

struct Model {
    @WrappedAppStorage(wrappedValue: true, "Test") var isEnabled
}

struct MyView: View {
    
    init(_ model: Model) {
        self._model = .init(initialValue: model)
        _valueFromModel = model.$isEnabled
    }

    @State var model: Model
    
    @AppStorage var valueFromModel: Bool
    @AppStorage(wrappedValue: true, "Test") var valueFromPreferences
    
    var body: some View {
        Form {
            Toggle("State", isOn: $valueFromModel)
            Toggle("Preferences", isOn: $valueFromPreferences)
        }
        .animation(.default)
    }
}

PlaygroundPage.current.setLiveView(MyView(Model()))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment