@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
Sure you can use both! Just compose the wrappers, but keep in mind that your binding won‘t be of type T but of type UserDefault<T>. Also for SwiftUI to recognize State, it must be the outer most wrapper.
@State @UserDefaulr(key: ..., defaultValue: ...)
var property: T
// `$property` will be of type `UserDefault<T>`
Can‘t really verify from my phone (for like a week).
I thought it was implemented, maybe it was reverted due some issues?
Does the composition for the following simple wrapper also fail?
@propertyWrapper
struct W<V> {
var wrappedValue: V
}
struct T {
@W @W var property: String = "swift"
}
If that feature does not work you can write the boilerplate yourself and delete it when it becomes available:
private var _property = State(wrappedValue: UserDefault(...))
var property: T {
get { return _propety.wrappedValue.wrappedValue }
set { _propety.wrappedValue.wrappedValue = newValue }
}
Simply composing @State @UserDefault will not do what I want in the end .
@State @UserDefaulr(key: ..., defaultValue: "a string")
var property: T // <== T is a String
One of the use is with TextField("Field Name", text: $property) or any other form widgets, for TextField$property is a Binding<String>, but it's a Binding<UserDefault<String>>. Anyway, the end goal is when TextField or any other widgets make edit, the change is propagate back and persist to UserDefaults. But just composing wrapper of a wrapper, the whole thing just doesn't work.
Can I subscribe to @State change event? It must have publisher inside.
You can use $property.wrappedValue to get the Binding of type T.
Edit: Keep in mind that Binding itself has a wrappedValue property which returns just Value, that means there will be a collision and the compiler will prefer the type member rather the dynamic key-path subscript. To resolve the collision just provide the type you want or use it in a context where you expect Binding<T>. That way the compiler will opt in and use $property[dynamicMember: \.wrappedValue] which is the same as $property.wrappedValue as Binding<T>.
I got the double-wrapper method working on Swift 5.2, with some tweaks. The big thing was changing the UserDefault propertyWrapper so that it took its default value as the normal Swift value initializer, instead of as a parameter in the propertyWrapper, so that the compiler would understand that my @State @UserDefaults do have initial values.
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
public struct PreferencesUIView : View {
@State @UserDefault(key: "usePaperLook") var usePaperLook: Bool = true
public var body: some View {
Toggle(isOn: $usePaperLook[dynamicMember: \.wrappedValue]) {
Text("Use Paper Texture")
}
}
}
I verified it writes the default value out and reads it back correctly. The only missing piece for me now is having this auto-detect when this default changes (from somewhere else in the program, or even another process) and trigger an update when it does.
[Edit: I discovered this only works when the Toggle button is in .toggleStyle(SwitchToggleStyle()), so this code does NOT work for the general case — it is apparently missing something, like my code attempt below.]
If I use $usePaperLook.wrappedValue I get the error:
Cannot convert value of type 'UserDefault' to expected argument type 'Binding'
Which seems like it makes sense to me since the UserDefault wrapper is inside the State wrapper, but then I don’t understand what’s going on under the hood enough to know why $usePaperLook[dynamicMember: \.wrappedValue] works.
I also can’t reverse the order of @State and @UserDefault without errors, but that may just be due to my naive implementation of @UserDefault / random guesses at how this all works.
[I also tested `$usePaperLook.wrappedValue as Binding<Bool>` and the compiler hates that as well, with the same error as the first attempt in this post.]
Ah sorry my bad. The key-path member lookup proposal says:
Key path member lookup only applies when the @dynamicMemberLookup type does not contain a member with the given name.
So basically this is exactly like in your case and where the collision occurs.
@dynamicMemberLookup
struct S<Value> {
let value: Value
subscript<Subject>(
dynamicMember keyPath: WritableKeyPath<Value, Subject>
) -> S<Subject> {
S<Subject>(value: value[keyPath: keyPath])
}
}
struct T {
let value: String
}
let s_of_t = S(value: T(value: "swift"))
// error: Cannot convert value of type 'T' to specified type 'String'
let string: String = s_of_t.value
For your example then only $usePaperLook[dynamicMember: \.wrappedValue] is the right solution, if you don't want to wrap the Binding manually. Sorry for the confusion.
I’m not sure I understand enough to wrap the Binding manually — what would that entail? I’m looking at the (purported) source code for @State and I see:
public var projectedValue: Binding<Value> {
return Binding(get: { return self.wrappedValue }, set: { newValue in self.wrappedValue = newValue })
}
But I’m not sure if this is related to what you’re saying.
First you get the project value as a Binding through _usePaperLook.projectedValue, but you reference it as $usePaperLook. Then Binding has a required subscript (but not necessarily in that form) due to the @dynamicMemberLookup attribute. For more information here are the related proposals:
Ok, I read the swift-evolution proposals and played with the above some, but decided this approach was too complicated (for me!) for the specific thing I’m trying to accomplish, which is reading and setting a UserDefault, which are always one level deep (eg, don’t really need paths, AFICT?).
I switched to a different approach of ONLY having a UserDefault<Value>@propertyWrapper (no more @State) and as the cruelest words in the universe go: it ALMOST works.
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(wrappedValue value: Value, key: String) {
self.defaultValue = value
self.key = key
}
var wrappedValue: Value {
get { UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue }
nonmutating set { UserDefaults.standard.set(newValue, forKey: key) }
}
var projectedValue: Binding<Value> {
Binding<Value>( get: { self.wrappedValue }, set: { newValue in self.wrappedValue = newValue } )
}
}
public struct PreferencesUIView : View {
@UserDefault(key: "usePaperLook") private var usePaperLook: Bool = true
public var body: some View {
Toggle(isOn: $usePaperLook) { Text("Use Paper Texture") }
}
}
This compiles AND displays the correct state on launch AND lets you toggle the state of the preference on disk...BUT when you toggle the toggle it only changes state for a millisecond and then goes back to whatever the launch state was (even though the UserDefault was, in fact, changed in the database).
So obviously I’m missing something. Is this whole approach dumb? Anyone? Beuller? Anyone?
[Edit: cleaned up code a tiny bit but bug is still there.]
I don't know how SwiftUI View maintain state, but I think you must use @State. It's possible that inside SwiftUI, your View is "forgotten" because SwiftUI somehow sees no difference. So it's just keeping that old copy, not the one you operate on. This is only my guess. I hope someone knowledgeable tell you.