I'm building a settings form and am trying to find a good way to model things so that setting and getting values on the viewModel
get passed through directly to the UserDefaults
via a property wrapper. Here's a minimal example:
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.value(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
init(wrappedValue: T, _ key: String) {
self.key = key
self.defaultValue = wrappedValue
}
}
// ⚠️ I can't use @Published in addition to my custom @UserDefault here ⚠️
class ViewModel: ObservableObject {
@UserDefault("some-user-setting") var someUserSetting = false
@UserDefault("another-user-setting") var anotherUserSetting = true
// ...
}
struct MyView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Form {
Toggle("Some user setting", isOn: $viewModel.someUserSetting)
Toggle("Another user setting", isOn: $viewModel.anotherUserSetting)
}
}
}
Now my viewModel
is an ObservableObject
but doesn't use @Published
anywhere because if I try to combine the two (@Published @UserDefault(...) ...
) I get an error Key path value type 'Bool' cannot be converted to contextual type 'UserDefault<Bool>'.
I suppose I could append a { didSet { objectWillChange.send() } }
after each property on my viewModel
to trigger the updates but that's also very ugly.
Importantly, I need to support iOS 13, otherwise I could use @AppStorage which is available from iOS 14+.
How would you model this elegantly and efficiently? Any ideas?