Swift 属性 包装器可以引用 属性 其包装的所有者吗?

Can a Swift Property Wrapper reference the owner of the property its wrapping?

在 Swift 的 属性 包装器中,有人可以回顾 class 的实例或拥有被包装的 属性 的打击吗?使用 self 显然行不通,super 也行不通。

我试图将 self 传递给 属性 包装器的 init() 但这也不起作用,因为 self on Configuration 还没有在评估 @propertywrapper 时定义。

我的用例是 class 用于管理大量设置或配置。如果有任何 属性 发生了变化,我只是想通知感兴趣的人 某事 发生了变化。他们真的不需要知道哪个值,所以对每个 属性 使用 KVOPublisher 之类的东西并不是真正必要的。

属性 包装器看起来很理想,但我不知道如何将某种引用传递给包装器可以回调到的拥有实例。

参考文献:

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

答案是否定的,目前的规范是不可能的。

我想做类似的事情。我能想到的最好办法是在 init(...) 末尾的函数中使用反射。至少这样你可以注释你的类型并且只在 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
            }
        }
    }
}

您目前无法开箱即用。

但是,您提到的提案在最新版本中将此作为未来的方向进行了讨论: https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type

目前,您可以使用 projectedValueself 分配给。 然后,您可以在设置 wrappedValue.

后使用它来触发某些操作

举个例子:

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)

我的实验基于: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

答案是肯定的!参见

使用 UserDefaults 包装器调用 ObservableObject 发布者的示例代码:

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