29

From within a property wrapper in Swift, can you someone refer back to the instance of the class or struck that owns the property being wrapped? Using self doesn't obviously work, nor does super.

I tried to pass in self to the property wrapper's init() but that doesn't work either because self on Configuration is not yet defined when @propertywrapper is evaluated.

My use case is in a class for managing a large number of settings or configurations. If any property is changed, I just want to notify interested parties that something changed. They don't really need to know which value just, so use something like KVO or a Publisher for each property isn't really necessary.

A property wrapper looks ideal, but I can't figure out how to pass in some sort of reference to the owning instance that the wrapper can call back to.

References:

SE-0258

enum PropertyIdentifier {
  case backgroundColor
  case textColor
}

@propertyWrapper
struct Recorded<T> {
  let identifier:PropertyIdentifier
  var _value: T

  init(_ identifier:PropertyIdentifier, defaultValue: T) {
    self.identifier = identifier
    self._value = defaultValue
  }

  var value: T {
    get {  _value }
    set {
      _value = newValue

      // How to callback to Configuration.propertyWasSet()?
      //
      // [self/super/...].propertyWasSet(identifier)
    }
  }
}

struct Configuration {

  @Recorded(.backgroundColor, defaultValue:NSColor.white)
  var backgroundColor:NSColor

  @Recorded(.textColor, defaultValue:NSColor.black)
  var textColor:NSColor

  func propertyWasSet(_ identifier:PropertyIdentifier) {
    // Do something...
  }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
kennyc
  • 5,490
  • 5
  • 34
  • 57
  • For the use case you describe, I'd find a `didSet` property observer simpler. If you need to annotate 1000 properties with the `Recorded` wrapper and have to adjust the you can just as well cut & paste the `didSet { self.propertyWasSet(.textColor) }` -- you could even consider ditching the `PropertyIdentifier` and use `KeyPath`s instead if that works for you. – ctietze Jun 20 '19 at 14:02
  • I'm hoping to avoid copy/pasting because the final property wrapper will contain additional logic like not notifying observers if the newValue is the same as the oldValue as well as perform some sanitation and validation on the property. An existing Objective-C implementation uses a build script to auto-generate the `.m` implementation but I was hoping for a more Swift'y solution. – kennyc Jun 20 '19 at 14:52
  • Then I'd still use a `didSet` property observer: add the diffing to your helper function and call it with `propertyWasSet(.textColor, oldValue, textColor)` to do its thing. This is a somewhat stateful operation. Some call the diffing part of a _view model_ already; and the fact that `Configuration` is subscribing to its own changes makes this no less a reactive binding situation. You could lift this knowledge into a type that wraps the property, e.g. `Binding` and pass `self` into that. – ctietze Jun 20 '19 at 15:50
  • 1
    Have a look at a plain Swift approach from 2014: http://rasic.info/bindings-generics-swift-and-mvvm/ -- also, maybe Sourcery or SwiftGen could help with actual code generation :) My personal preference is to separate the state from the event hub, e.g. use KVO or similar on all properties but then do not forward any of the detail to actual subscribers. – ctietze Jun 20 '19 at 15:51
  • 1
    I can appreciate that there might be better design patterns appropriate for the very basic example above, but that doesn't really address the core question which is if a property wrapper can access the wrapped property's instance. There are many times where the setting of one property might be dependent on the value of other properties within the same model. If that pattern is frequent enough in a code base then it warrants being factored out into some sort of reusable component. Property wrappers *might* be ideal for this, which is what I'm trying to figure out. – kennyc Jun 20 '19 at 16:53
  • Ok. At least I made sure you didn't just look for a solution to the underlying problem, but really are looking for this exact implementation question :) – ctietze Jun 21 '19 at 05:47

4 Answers4

13

The answer is no, it's not possible with the current specification.

I wanted to do something similar. The best I could come up with was to use reflection in a function at the end of init(...). At least this way you can annotate your types and only add a single function call in init().


fileprivate protocol BindableObjectPropertySettable {
    var didSet: () -> Void { get set }
}

@propertyDelegate
class BindableObjectProperty<T>: BindableObjectPropertySettable {
    var value: T {
        didSet {
            self.didSet()
        }
    }
    var didSet: () -> Void = { }
    init(initialValue: T) {
        self.value = initialValue
    }
}

extension BindableObject {
    // Call this at the end of init() after calling super
    func bindProperties(_ didSet: @escaping () -> Void) {
        let mirror = Mirror(reflecting: self)
        for child in mirror.children {
            if var child = child.value as? BindableObjectPropertySettable {
                child.didSet = didSet
            }
        }
    }
}
arsenius
  • 12,090
  • 7
  • 58
  • 76
7

You cannot do this out of the box currently.

However, the proposal you refer to discusses this as a future direction in the latest version: https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type

For now, you would be able to use a projectedValue to assign self to. You could then use that to trigger some action after setting the wrappedValue.

As an example:

import Foundation

@propertyWrapper
class Wrapper {
    let name : String
    var value = 0
    weak var owner : Owner?

    init(_ name: String) {
        self.name = name
    }

    var wrappedValue : Int {
        get { value }
        set {
            value = 0
            owner?.wrapperDidSet(name: name)
        }
    }

    var projectedValue : Wrapper {
        self
    }
}


class Owner {
    @Wrapper("a") var a : Int
    @Wrapper("b") var b : Int

    init() {
        $a.owner = self
        $b.owner = self
    }

    func wrapperDidSet(name: String) {
        print("WrapperDidSet(\(name))")
    }
}

var owner = Owner()
owner.a = 4 // Prints: WrapperDidSet(a)
tdekker
  • 71
  • 1
  • 2
4

My experiments based on : https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type

protocol Observer: AnyObject {
    func observableValueDidChange<T>(newValue: T)
}

@propertyWrapper
public struct Observable<T: Equatable> {
    public var stored: T
    weak var observer: Observer?

    init(wrappedValue: T, observer: Observer?) {
        self.stored = wrappedValue
    }

    public var wrappedValue: T {
        get { return stored }
        set {
            if newValue != stored {
                observer?.observableValueDidChange(newValue: newValue)
            }
            stored = newValue
        }
    }
}

class testClass: Observer {
    @Observable(observer: nil) var some: Int = 2

    func observableValueDidChange<T>(newValue: T) {
        print("lol")
    }

    init(){
        _some.observer = self
    }
}

let a = testClass()

a.some = 4
a.some = 6
Robert Koval
  • 481
  • 4
  • 4
  • Robert, this was absolutely brilliant! In my case I need to use the delegate for both get and set, so I'm going to fatalError() if the delegate is nil to catch time where a new property is added and the init doesn't set the delegate. Excellent just excellent!!! – David H Jun 29 '22 at 21:40
4

The answer is yes! See this answer

Example code for calling ObservableObject publisher with a UserDefaults wrapper:

import Combine
import Foundation

class LocalSettings: ObservableObject {
  static var shared = LocalSettings()

  @Setting(key: "TabSelection")
  var tabSelection: Int = 0
}

@propertyWrapper
struct Setting<T> {
  private let key: String
  private let defaultValue: T

  init(wrappedValue value: T, key: String) {
    self.key = key
    self.defaultValue = value
  }

  var wrappedValue: T {
    get {
      UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }

  public static subscript<EnclosingSelf: ObservableObject>(
    _enclosingInstance object: EnclosingSelf,
    wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
    storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
  ) -> T {
    get {
      return object[keyPath: storageKeyPath].wrappedValue
    }
    set {
      (object.objectWillChange as? ObservableObjectPublisher)?.send()
      UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
    }
  }
}
Nicolas Degen
  • 1,522
  • 2
  • 15
  • 24