[SOLVED] Swift – How to create a class-constrained property wrapper?

Issue

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

Solution

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

Answered By – Sweeper

Answer Checked By – Marie Seifert (BugsFixing Admin)

Leave a Reply

Your email address will not be published.