Property wrapper trouble

I'm trying to make a property wrapper for accessing the string value of a control owned by a view controller. So far I have this:

@propertyWrapper struct ControlStringValue<Container> where Container: AnyObject
{
  private let container: Container
  private let keyPath: ReferenceWritableKeyPath<Container, NSControl?>
  
  var wrappedValue: String
  {
    get { return container[keyPath: keyPath]?.stringValue ?? "" }
    set { container[keyPath: keyPath]?.stringValue = newValue }
  }
}

class MyViewController: NSViewController
{
  @IBOutlet var someControl: NSControl!
  
  @ControlStringValue(container: self, keyPath: \MyViewController.someControl) var text: String
}

But I get this error:

error: Sandbox.playground:20:53: error: cannot convert value of type 'ReferenceWritableKeyPath<MyViewController, NSControl?>' to expected argument type 'ReferenceWritableKeyPath<_, NSControl?>'
@ControlStringValue(container: self, keyPath: \MyViewController.someControl) var text: String

So it seems like it's not fully inferring the generic type. What else can I do to nudge the compiler?

An alternative would be to store the control in the wrapper later, once the view controller is fully initialized, but that doesn't seem as clean.

There are two issues:

  1. The synthesised init is private
  2. You're passing self in a property initializer

You're getting a bogus error because of the first issue. If you remove container, you'll get the actual error:

'ControlStringValue' initializer is inaccessible due to 'private' protection level

Once you've fixed that, I think there's two ways to fix (2):

  1. You can use the _enclosingInstance static subscript, but keep in mind it's a prototype and can change behaviour or stop working as it's not official yet.

  2. You can assign self after the view loads.

Here's how you can do (2):

@propertyWrapper
struct ControlStringValue<Container: AnyObject> {
  private var container: Container?
  private let keyPath: ReferenceWritableKeyPath<Container, NSControl?>

  init(keyPath: ReferenceWritableKeyPath<Container, NSControl?>) {
    self.keyPath = keyPath
  }

  var wrappedValue: String {
    get { return container?[keyPath: keyPath]?.stringValue ?? "" }
    set { container?[keyPath: keyPath]?.stringValue = newValue }
  }

  var projectedValue: Container? {
	get { container }
	set { container = newValue }
  }
}

final class MyViewController: NSViewController {
  @IBOutlet var someControl: NSControl!
  @ControlStringValue(keyPath: \MyViewController.someControl) var text: String

  override func viewDidLoad() {
	super.viewDidLoad()
	$text = self
  }
}

Thanks! I think I'll go for the second option until enclosingInstance is official.

1 Like