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

Posted

技术标签:

【中文标题】Swift 属性包装器可以引用其包装的属性所有者吗?【英文标题】:Can a Swift Property Wrapper reference the owner of the property its wrapping? 【发布时间】:2019-11-03 06:39:00 【问题描述】:

在 Swift 的属性包装器中,您是否可以参考拥有被包装属性的类的实例或被击中?使用self 显然不起作用,super 也不起作用。

我尝试将self 传递给属性包装器的init(),但这也不起作用,因为在评估@propertywrapper 时尚未定义Configuration 上的self

我的用例属于管理大量设置或配置的类。如果有任何属性发生变化,我只想通知相关方某事发生了变化。他们实际上并不需要只知道哪个值,因此对于每个属性都使用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...
  

【问题讨论】:

对于您描述的用例,我会发现didSet 属性观察器更简单。如果您需要使用 Recorded 包装器注释 1000 个属性并且必须调整它们,您也可以剪切和粘贴 didSet self.propertyWasSet(.textColor) - 您甚至可以考虑放弃 PropertyIdentifier 并使用 KeyPaths 代替适合你。 我希望避免复制/粘贴,因为最终的属性包装器将包含额外的逻辑,例如如果 newValue 与 oldValue 相同则不通知观察者,并对属性执行一些清理和验证.现有的 Objective-C 实现使用构建脚本自动生成 .m 实现,但我希望有一个更 Swift'y 的解决方案。 那么我仍然会使用didSet 属性观察者:将差异添加到您的辅助函数并使用propertyWasSet(.textColor, oldValue, textColor) 调用它来完成它的工作。这是一个有点状态的操作。有些人已经把 view model 的差异部分称为了;并且Configuration 正在订阅自己的更改这一事实使得这同样是一种反应性绑定情况。您可以将此知识提升为包装属性的类型,例如Binding&lt;NSColor, Configuration&gt; 并将 self 传递给它。 看看 2014 年的普通 Swift 方法:rasic.info/bindings-generics-swift-and-mvvm - 另外,也许 Sourcery 或 SwiftGen 可以帮助实际代码生成 :) 我个人的偏好是将状态与事件中心分开,例如在所有属性上使用 KVO 或类似方法,但不要将任何详细信息转发给实际订阅者。 我可以理解,可能有更好的设计模式适用于上述非常基​​本的示例,但这并不能真正解决核心问题,即属性包装器是否可以访问被包装属性的实例。很多时候,一个属性的设置可能取决于同一模型中其他属性的值。如果该模式在代码库中足够频繁,那么它保证被分解为某种可重用的组件。属性包装器可能对此很理想,这是我想要弄清楚的。 【参考方案1】:

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

我想做类似的事情。我能想到的最好的办法是在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
            
        
    

【讨论】:

【参考方案2】:

您目前无法立即执行此操作。

但是,您提到的提案在最新版本中将此作为未来方向进行了讨论: 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)

【讨论】:

【参考方案3】:

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

【讨论】:

【参考方案4】:

答案是肯定的!见this answer

使用 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)
    
  

【讨论】:

以上是关于Swift 属性包装器可以引用其包装的属性所有者吗?的主要内容,如果未能解决你的问题,请参考以下文章

Swift之深入解析SwiftUI属性包装器如何处理结构体

Swift 属性包装器

Swift 属性包装器

在 Swift 属性包装器中公开字典

依赖键路径的KVO对Swift类不起作用

原子属性包装器仅在声明为类而不是结构时才有效