What the best way to achieve @UserDefaults @State?

I need my var to have both @UserDefaults @State functionality without change any existing code that's already using the var as @State.

I can get the @UserDefaults code from the Property Wrapper Proposal: https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#user-defaults

@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)
    }
  }
}

Can I modify this to add @State to it?

You can't use @State, this var is only writeable from the SwiftUI-Thread. But you could add a ViewModel:

final class UserSettings: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    @UserDefault(key: "text", defaultValue: "")
    var text: String

    private var notificationSubscription: AnyCancellable?

    init() {
        notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
            self.objectWillChange.send()
        }
    }
}

This ViewModel will also fire, if you use UserDefaults.set(_:forKey:).

1 Like

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>`

@State @UserDefault(key: "foo", default: "here here") var foo

Error:

Multiple property wrappers are not supported

The proposal said composition is not support for now, which is the reason I ask the original question.

The proposal suggest to simulate the two. But I don't know how @State is inside.

Or @State @UserDefault should work after all? Am I doing something wrong?

That‘s weird wrappers composition was implemented, there are a bunch of tests for that on the master. Which Xcode beta are you using?

Version 11.0 beta 6 (11M392q)

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 }
}

It was temporarily disabled due to pending bugs: (https://github.com/apple/swift/pull/26288), and enabled again after those bugs were fixed (https://github.com/apple/swift/pull/26326). However, that fix wasn’t cherry picked to 5.1 hence why it’s still disabled on 5.1, but works on master.

1 Like

It doesn‘t look like that wrappers composition will land in Swift 5.1.

Cc @Joe_Groff still any plans or is it too late/risky now?

Simply composing @State @UserDefault will not do what I want in the end :angry:.

@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>.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy