Closed Lumisilk closed 1 year ago
Thanks for working on this đź‘Ť
Have you considered integrating this into @Default
, like with @AppStorage
? That would be preferable.
Due to this limitation, @PublishedDefault CANNOT observe changes in the value of the underlying UserDefaults, and can only trigger objectWillChange if the value change is made by the ObservableObject that holds it.
Maybe something like this: https://github.com/mergesort/Boutique/blob/ad47507dc0e1c4cf1693fd08567457d9caf664b7/Sources/Boutique/Stored.swift#L32-L44
@Default
Since @Published
's projectedValue should be Publisher<Value, Never>
but @Default
for SwiftUI is Binding<Value>
, I think the two feature cannot be integrated into one @Default
.
I tried this:
public struct PublishedDefault<Value: _DefaultsSerializable> {
private var cancellable: AnyCancellable?
public static subscript<Object: ObservableObject>(
_enclosingInstance instance: Object,
wrapped _: ReferenceWritableKeyPath<Object, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Object, Self>
) -> Value where Object.ObjectWillChangePublisher == ObservableObjectPublisher {
get {
subscribeDefault(to: instance, storage: storageKeyPath)
return instance[keyPath: storageKeyPath].value
}
set {
subscribeDefault(to: instance, storage: storageKeyPath)
instance[keyPath: storageKeyPath].value = newValue
}
}
public static func subscribeDefault<Object: ObservableObject>(
to instance: Object,
storage storageKeyPath: ReferenceWritableKeyPath<Object, Self>
) where Object.ObjectWillChangePublisher == ObservableObjectPublisher {
guard instance[keyPath: storageKeyPath].cancellable == nil else {
return
}
// transfer Defaults publisher to objectWillChange
instance[keyPath: storageKeyPath].cancellable =
Defaults.publisher(instance[keyPath: storageKeyPath].key, options: [.prior])
.sink { [weak instance] change in
guard let instance, change.isPrior else { return }
instance.objectWillChange.send()
}
}
// Use Defaults.Publisher as projectedValue directly
}
Making @PublishedDefault
catch a reference to the object when read/write occurs, observation works.
But ONLY when the read/write occurred at least once.
Like this:
let viewModel = ViewModel()
Defaults[.opacity] = 1
viewModel.$opacity // not fire
viewModel.objectWillChange // not fire
viewModel.opacity = 1
// now observation starts working
viewModel.$opacity // fire
viewModel.objectWillChange // fire
I'm afraid I cannot find any solution about observation…
Since @Published's projectedValue should be Publisher<Value, Never> but @Default for SwiftUI is Binding
, I think the two feature cannot be integrated into one @Default.
I wonder how @AppStorage
is able to do this then. Maybe they're using some internal hooks.
Having it be @PublishedDefault
is fine.
And you sure the subscript is not called during initialisation?
What if in the normal init()
we call Self.subscribe()
to start listening? Where Self.subscribe()
is a static method that sets up the listening once.
I wonder how
@AppStorage
is able to do this then
I didn't know AppStorage become compatible with ObservableObject from iOS 14.5. (missed the detail link of reply in #48)
And I finally catch up what's the problem here.
It seems that @Published
didn't use this enclosing feature. The objectWillChange
's get method injects itself to the @Published using the runtime introspection.
We cannot inject the code to the default implementation of ObservableObject.objectWillChange
, so there doesn't seem to be a perfect solution:
@Default
can't be compatible with ObservableObject
@AppStorage
hard to cover Codable@PublishedDefault
can't observe the changeIs my understanding right?
And you sure the subscript is not called during initialisation?
The static subscript, Yes, it's not.
What if in the normal init() we call Self.subscribe() to start listening? Where Self.subscribe() is a static method that sets up the listening once.
What's the Self
here? ObservableObject
?
Is my understanding right?
Yes
What's the Self here? ObservableObject?
PublishedDefault
Sorry I didn't get your point…
What I want is a property wrapper that can trigger the owner ObservableObject
's objectWillChange
fire when the corresponding default value changed.
And static subscript(_enclosingInstance instance:, wrapped _:, storage storageKeyPath:)
is the only place where you can access the owner object from the property wrapper side. init()
or Self.someStaticMethod()
can't do it.
This is ugly, but one workaround may be to tell the user to do this:
final class ViewModel: ObservableObject {
@PublishedDefault(.opacity) var opacity
init() {
opacity = opacity
}
}
Since @Published's projectedValue should be Publisher<Value, Never> but @Default for SwiftUI is Binding
, I think the two feature cannot be integrated into one @Default.
Thinking more about this. That is just a requirement for @Published
, not for a property wrapper that wants to trigger an ObservableObject
.
With the latest news about @Observable
, I don't think the current approach is going to work out. See: https://github.com/sindresorhus/Defaults/issues/142
Closing this. Let's continue the discussion in #142.
Introduction
Hi. Thanks for the awesome Defaults framework!
This PR add a new property wrapper
@PublishedDefault
, that is used exclusively forObservableObject
.@PublishedDefault
will triggerobjectWillChange
when changing the value of Defaults viaObservableObject
.You can use it like this
Detail
I used a feature called Referencing the enclosing 'self' in a wrapper type that is not in the Swift documentation but is in the Property Wrapper's Proposal. This feature makes the property wrapper can access the object that holding itself.
However, you can access the enclosing object only when that object reads/write the corresponding value, instead of all the time.
Due to this limitation,
@PublishedDefault
CANNOT observe changes in the value of the underlyingUserDefaults
, and can only triggerobjectWillChange
if the value change is made by theObservableObject
that holds it.(Well, technically you can let
@PublsihedDefaults
hold a reference to the object when read/write occurs to make the observation work. But that doesn't smell good, so I didn't try it.)It would be nice if you can also check the comments.
This PR also fix #48
Other helpful reference:
https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ https://www.avanderlee.com/swift/appstorage-explained/#creating-an-alternative-solution-for-reading-and-writing-user-defaults-through-a-property-wrapper
TODO
AnyObject
as generic constraintObservableObject
if possible, instead of constrain it asObservableObject
only.@Defaults
comment and README