Reputation: 4362
I wish to apply a custom property wrapper to a variable already wrapped in @Published
, nesting them like
(A) @Custom @Published var myVar
or
(B) @Published @Custom var myVar
(notice the application order of the wrappers).
In the case of (A) I get the error
'wrappedValue' is unavailable: @Published is only available on properties of classes
and for (B)
error: key path value type 'Int' cannot be converted to contextual type 'Updating<Int>'
neither of which are particularly helpful. Any ideas how to make it work?
import Combine
class A {
@Updating @Published var b: Int
init(b: Int) {
self.b = b
}
}
@propertyWrapper struct Updating<T> {
var wrappedValue: T {
didSet {
print("Update: \(wrappedValue)")
}
}
}
let a = A(b: 1)
let cancellable = a.$b.sink {
print("Published: \($0)")
}
a.b = 2
// Expected output:
// ==> Published: 1
// ==> Published: 2
// ==> Update: 2
Upvotes: 10
Views: 3654
Reputation: 13
I tried MechEthan's solution and it didn't work for me... and I figured out by Simone's answer that I still need to send the objectWillChange
signal. So I ended up with this:
@propertyWrapper
class _CustomPublished<Instance: ObservableObject, Value> where Instance.ObjectWillChangePublisher == ObservableObjectPublisher {
@available(*, unavailable)
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
var projectedValue: Published<Value>.Publisher {
get { $value }
set { $value = newValue }
}
static subscript(
_enclosingInstance instance: Instance,
wrapped _: KeyPath<Instance, Value>,
storage storageKeyPath: KeyPath<Instance, _CustomPublished>
) -> Value {
get { instance[keyPath: storageKeyPath].value }
set {
instance.objectWillChange.send()
instance[keyPath: storageKeyPath].value = newValue
}
}
init(wrappedValue: Value) {
value = wrappedValue
}
@Published private var value: Value
}
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
typealias CustomPublished<T> = _CustomPublished<Self, T>
}
I replaced some @Published
in my project and it seems working, but not sure whether there's some other hidden differences
Really, swift should have done better in composing property wrappers
Upvotes: 0
Reputation: 5703
The only solution I've found is a workaround: Make a custom @propertyWrapper
that has a @Published
property inside of it.
Example:
/// Workaround @Published not playing nice with other property wrappers.
/// Use this to replace @Published to temporarily help debug a property being accessed off the main thread.
@propertyWrapper
public class MainThreadPublished<Value> {
@Published
private var value: Value
public var projectedValue: Published<Value>.Publisher {
get {
assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
return $value
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
set {
assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
$value = newValue
}
}
public var wrappedValue: Value {
get {
assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
return value
}
set {
assert(Thread.isMainThread, "Accessing @MainThread property on wrong thread: \(Thread.current)")
value = newValue
}
}
public init(wrappedValue value: Value) {
self.value = value
}
public init(initialValue value: Value) {
self.value = value
}
}
Further reading:
EDIT:
I also just found this article that may provide an alternate approach, but I don't have time to investigate:
Upvotes: 2
Reputation: 111
None of the options you provided can be applicated to make a custom property wrapper behave like it was marked with @Published (A and B)
The real question is, how do i observe property/status update changes?
Using @Published wrapper, which handles status update automatically
Tracking the status update manually, by implementing
willSet {objectWillChange.send()}
Considering the option 1 cant work as you cant apply 2 property wrappers, you can go for manual status update tracking. In order to accomplish this, you will need to make your class conforming the ObservableObject protocol.
class A: ObservableObject {
@Updating var b: Int{
willSet {objectWillChange.send()}
}
init(b: Int) {
self.b = b
}
}
Your views will now be able to refresh whenever var b changes, having at the same time your @Updating wrapper fully working.
Upvotes: 2