【问题标题】:How to observe individual array element changes (update) with Swift and KVO?如何使用 Swift 和 KVO 观察单个数组元素的变化(更新)?
【发布时间】:2016-05-29 08:26:56
【问题描述】:

我是否需要订阅/取消订阅数组的各个元素?

我想单独更新每个表格视图单元格以反映支持数组中的更改。通过更改,我的意思不是追加/删除操作,而是更新数组对象的属性。 我希望我能够解释我想要实现的目标。 谢谢

【问题讨论】:

    标签: ios swift key-value-observing


    【解决方案1】:

    要使用 KVO,请使用 dynamic 属性声明模型对象:

    class Foo: NSObject {
        @objc dynamic var bar: String   // in Swift 3, `@objc` is not necessary; in Swift 4 we must make this explicit
    
        init(bar: String) {
            self.bar = bar
            super.init()
        }
    }
    

    然后,让单元处理 KVO。首先,我有一个协议,单元格可以通过该协议通知表格视图它需要重新加载:

    protocol CustomCellDelegate: class {
        func didUpdateObject(for cell: UITableViewCell)
    }
    

    表格视图控制器可以遵循CustomCellDelegate 协议,并在需要时重新加载单元格:

    func didUpdateObject(for cell: UITableViewCell) {
        if let indexPath = tableView.indexPath(for: cell) {
            tableView.reloadRows(at: [indexPath], with: .fade)
        }
    }
    

    所以,然后定义单元格来设置和处理 KVO。在 Swift 4 和 iOS 11 中,您可以将基于闭包的 observe 方法与新的强类型键一起使用:

    class CustomCell: UITableViewCell {
    
        weak var delegate: CustomCellDelegate?
    
        private var token: NSKeyValueObservation?
    
        var object: Foo? {
            willSet {
                token?.invalidate()
            }
            didSet {
                textLabel?.text = object?.bar
                token = object?.observe(\.bar) { [weak self] object, change in
                    if let cell = self {
                        cell.delegate?.didUpdateObject(for: cell)
                    }
                }
            }
        }
    }
    

    在 Swift 3 中:

    class CustomCell: UITableViewCell {
    
        private var observerContext = 0
    
        weak var delegate: CustomCellDelegate?
    
        var object: Foo? {
            willSet {
                object?.removeObserver(self, forKeyPath: #keyPath(Foo.bar), context: &observerContext)
            }
            didSet {
                textLabel?.text = object?.bar
                object?.addObserver(self, forKeyPath: #keyPath(Foo.bar), context: &observerContext)
            }
        }
    
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            guard context == &observerContext else {
                super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
                return
            }
    
            delegate?.didUpdateObject(for: self)
        }
    
        deinit {
            object?.removeObserver(self, forKeyPath: #keyPath(Foo.bar), context: &observerContext)
        }
    
    }
    

    现在cellForRowAtIndexPath 可以只设置delegate 和object 属性:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
    
        cell.delegate = self
        cell.object = objects![indexPath.row]
    
        return cell
    }
    

    在我看来,这种 KVO 机制比 Swift 观察者机制更好,因为模型对象不需要(也不应该)知道任何关于观察者的信息。它只需要表明它支持动态调度,就是这样。

    有关上述内容的 Swift 2 版本,请参阅 previous revision of this answer。

    【讨论】:

    • 谢谢罗伯。我真的很喜欢你的方法。
    • 本例中的 &observerContext 是什么,它从何而来?
    • @Eden - observeValue(forKeyPath:of:change:context:) 需要一些“上下文”来知道这个通知是它正在观察的,还是它的 super 类正在观察的东西。所以我们在创建观察者时指定一个唯一的“上下文”,并在处理通知时检查。执行此操作的标准方法是定义一些变量,在本例中为 observerContext,并将其内存地址用作唯一标识符。这是一种常见的模式,但如果不是很明显,我很抱歉。我已经在上面添加了它(以及提供更简单的基于块的 Swift 4、iOS 11 版本)。
    • @Eden - 我建议在添加和删除观察者的地方添加日志记录,并确保它们是平衡的,但一个常见的错误是var a = Foo(); a.addObserver(self, ...); a = Foo(); a.removeObserver(...);,即你可能正在处理相同的变量名称,但类的不同实例。观察者与对象的实例相关联,而不是变量名。如果不是这样,请发布您自己的问题。
    • @Rob:在 deinit 中删除观察者时,您不应该也使用上下文吗?对象?
    【解决方案2】:

    您可以在后备数组中存储的变量上使用 setter 观察者。

    var s = "myString" {
        willSet {
            // do the update on the cell here with newValue
        }
        didSet {
            // do something with oldValue
        }
    }
    var array: [String] = []
    array.append(s)
    

    当您更改array 中的值时,将执行 willSet 和 didSet,您可以在单元格上调用一个函数来执行您想要的更新。

    【讨论】:

      【解决方案3】:

      您可以使用委托来处理单个单元格的数据(例如,数组的元素)和这些单元格的所有者(即数组的所有者)之间的通信。 “支持数组”中的更改可以传播到使用具有适当信息的委托回调所拥有的数组。

      例如:

      /* delegate protocol blueprinting delegate callback methods */
      protocol MyDelegate: class {
          func arrayEntryUpdated(element: Foo)
      }
      
      /* lets assume your table view cells data source are Foo objects */
      class Foo {
          var id: Int = -1
          private var bar : String = "" {
              didSet {
                  /* notify the delegate each time the bar
                     property of a Foo object is set */
                  delegate?.arrayEntryUpdated(self)
              }
          }
      
          weak var delegate: MyDelegate?
      }
      
      /* you can then hande individual updates of the Foo objects via
         delegate callbacks to the owner of the Foo array */
      class MyArrayOwningClass: MyDelegate {
          var fooArr : [Foo]
      
          init(fooArr: [Foo]) {
              self.fooArr = fooArr
              self.fooArr.enumerate().forEach { $1.id = $0; $1.delegate = self }
          }
      
          // MyDelegate
          func arrayEntryUpdated(element: Foo) {
              print("Foo element of id #\(element.id) updated.")
                  // ... in your case, handle individual cell updating
          }
      }
      

      示例用法:

      let fooArr = [Foo(), Foo(), Foo()] // "data source"
      let owner = MyArrayOwningClass(fooArr: fooArr)
      
      // update an element of the data source
      fooArr[1].bar = "bar" // via delegate: "Foo element of id #1 updated."
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-01-03
        • 2012-05-13
        • 1970-01-01
        • 1970-01-01
        • 2021-07-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多