I am trying to write a class-constrained property wrapper, much like @Published
offered by Combine.
For example:
struct Person {
// error: 'wrappedValue' is unavailable: @Published is only available on properties of classes
@Published var age = 0
}
I cannot figure out how to implement this behaviour myself.
Looking at the interface for Published
, there is no syntax indicating that the property wrapper is class-constrained:
@propertyWrapper public struct Published<Value> {
public init(wrappedValue: Value)
public init(initialValue: Value)
public struct Publisher : Publisher {
// ...
}
public var projectedValue: Published<Value>.Publisher { mutating get set }
}
Somehow the wrappedValue
is only being made available when the property wrapper is defined in an AnyObject
.
Because of the error message, it looks like this is making use of @available
in some way to be able to infer the context where this is defined, and is then making wrappedValue
conditionally unavailable if so.
Notice the wording of the error message:
@available(*, unavailable, message: "This is a test")
func foo() {}
foo() // error: 'foo()' is unavailable: This is a test
Is it possible to implement this behaviour myself?
// How to constrain to only use in classes?
@propertyWrapper
struct MyClassWrapper<Value> {
var state: Value
var wrappedValue: Value {
get {
state
}
set {
state = newValue
}
}
}
CodePudding user response:
From the behaviour I've seen, I think it is either simply marked unavailable
like this:
@available(*, unavailable, message: "@Published is only available on properties of classes")
var wrappedValue: Value
This is because you cannot access Published.wrappedValue
from anywhere. For example, even this gives the same "unavailable" error:
class Bar: ObservableObject {
@Published var x = ""
func f() {
// 'wrappedValue' is unavailable: @Published is only available on properties of classes
print(_x.wrappedValue)
}
}
Clearly, the error doesn't make much sense here, so it is probably the case that wrappedValue
itself is marked unavailable.
Properties marked with a property wrapper are usually lowered into a computed property of the wrapped type, and a stored property of the wrapper type, e.g.
@Published var x = "foo"
is lowered into:
var x: String {
get { _x.wrappedValue }
set { _x.wrappedValue = newValue }
}
private var _x = Published(wrappedValue: "foo")
(Source: SE-0258)
This is what happens when you try to use @Published
in a struct, and as you can see, it uses wrappedValue
, which produces the error. However, when you use @Published
in a class, I suspect that some compiler magic happens, and lowers it a different way, that doesn't involve wrappedValue
.
The compiler needs to do this special thing for classes, because the class is supposed to inherit ObservableObject
, and some compiler magic is required so that the default implementation of objectWillChange
can find all the @Published
properties.
In any case, it is also possible to write a Published
implementation that isn't class-bound, but of course you don't get the auto generated objectWillChange
implementation if you choose to do that. Here is an example adapted from here.
@propertyWrapper
struct Published<T> {
private var innerSubject = PassthroughSubject<T, Never>()
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
var wrappedValue: T {
didSet {
innerSubject.send(wrappedValue)
}
}
var projectedValue: AnyPublisher<T, Never> {
innerSubject.eraseToAnyPublisher()
}
}