如何使用 KVO 根据底层数组元素的变化来更新 tableViewCells?

How to use KVO to update tableViewCells based on underlying array element changes?

我有一个代表底层数组的 table 视图。这些单元格有一个标签和一个滑块,它应该显示数组的 percentage 属性 的值。

我想在百分比 属性 发生变化时使用键值观察来更新标签。 (我知道 KVO 在这个例子中有点矫枉过正,但最终滑动一个滑块会影响其他单元格,包括滑块的位置和底层数组将从应用程序中的多个位置随时设置,所以 KVO 是可行的方法。 )

我得到了很多帮助 ,但我无法启动它并更新标签。我在这里包括我所有的代码。不知道我哪里错了。

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellDelegate {

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self

        for i in 0...4 {
            items.append(Items(ID: i, percentage: 50))
        }
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        if let cell = tableView.dequeueReusableCell(withIdentifier: myTableViewCell.ID) as? myTableViewCell {
            cell.object = items[indexPath.row]
            cell.mySlider.tag = indexPath.row

            return cell

        } else {
            return UITableViewCell()
        }

    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }


    @IBAction func sliderValueChanged(_ sender: UISlider) {
        items[sender.tag].percentage = Double(sender.value)
        print("percentage at \(items[sender.tag].ID) is \(items[sender.tag].percentage)")
    }

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

class myTableViewCell: UITableViewCell {
    static let ID = "myCell"
    weak var delegate: CustomCellDelegate?
    private var token: NSKeyValueObservation?

    var object: Items? {
        willSet {
            token?.invalidate()
        }
        didSet {
            myLabel.text = "\(object?.percentage ?? 0)"
            token = object?.observe(\.percentage) { [weak self] object, change in
                if let cell = self {
                    cell.delegate?.didUpdateObject(for: cell)
                }
            }
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    @IBOutlet weak var myLabel: UILabel!
    @IBOutlet weak var mySlider: UISlider!

}

class Items: NSObject {
    let ID: Int
    @objc dynamic var percentage: Double

    init(ID: Int, percentage: Double){
        self.ID = ID
        self.percentage = percentage
        super.init()
    }
}

var items: [Items] = []


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

嗨@sean 问题在 UITableview 单元格中 class 你已经制作了 diSet 方法,所以你不需要为 cell.lable 和滑块传递值 只需尝试下面的代码

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    if let cell = tableView.dequeueReusableCell(withIdentifier: myTableViewCell.ID) as? myTableViewCell {
        //pass the object to which you wanna add observer to cell
        cell.object = items[indexPath.row]
        return cell
    } else {
        return UITableViewCell()
    }

}

要在 Swift 4 中执行 KVO,您必须将 属性 声明为 dynamic 并对该对象调用 observe(_:options:changeHandler:),保存结果 NSKeyValueObservation 令牌。当该令牌超出范围(或被另一个令牌替换)时,原始观察者将自动被删除。

在您的情况下,您让观察者调用委托,然后委托重新加载单元格。但是您似乎从来没有设置 delegate 属性,所以我怀疑没有调用该方法。

但这一切似乎都有些脆弱。我倾向于直接在观察者 changeHandler 中更新标签。我还认为您可以更直接地更新单元格(我将 "value changed" IBAction 放在单元格中,而不是 table 视图中),并消除 [=18 的相当尴尬的使用=] 以确定模型数组中的哪一行更新了滑块(如果插入或删除行,这可能会出现问题)。

所以考虑这个对象:

class CustomObject: NSObject {
    let name: String
    @objc dynamic var value: Float       // this is the property that the custom cell will observe

    init(name: String, value: Float) {
        self.name = name
        self.value = value

        super.init()
    }
}

然后您可以拥有一个 table 视图控制器,它使用此模型类型的实例填充 objects 的数组。这里的细节在很大程度上与观察无关(我们将在下面介绍),但我包括这个只是为了提供一个完整的例子:

class ViewController: UITableViewController {

    var objects: [CustomObject]!

    override func viewDidLoad() {
        super.viewDidLoad()

        // self sizing cells

        tableView.estimatedRowHeight = 60
        tableView.rowHeight = UITableViewAutomaticDimension

        // populate model with random data

        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut

        objects = (0 ..< 1000).map {
            CustomObject(name: formatter.string(for: [=11=])!, value: 0.5)
        }
    }
}

// MARK: - UITableViewDataSource

extension ViewController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return objects?.count ?? 0
    }

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

完成后,您现在可以为您的单元格设置基础 class (a) 如果滑块发生变化,则更新模型对象; (b) 观察 dynamic 属性 的变化,在这个例子中,当在模型对象中观察到 value 变化时更新标签:

class CustomCell: UITableViewCell {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var valueLabel: UILabel!
    @IBOutlet weak var valueSlider: UISlider!

    static private let formatter: NumberFormatter = {
        let _formatter = NumberFormatter()
        _formatter.maximumFractionDigits = 2
        _formatter.minimumFractionDigits = 2
        _formatter.minimumIntegerDigits = 1
        return _formatter
    }()

    private var token: NSKeyValueObservation?

    weak var object: CustomObject? {
        didSet {
            let value = object?.value ?? 0

            nameLabel.text = object?.name
            valueLabel.text = CustomCell.formatter.string(for: value)
            valueSlider.value = value

            token = object?.observe(\.value) { [weak self] object, change in
                self?.valueLabel.text = CustomCell.formatter.string(for: object.value)
            }
        }
    }

    @IBAction func didChangeSlider(_ slider: UISlider) {
        object?.value = slider.value
    }
}

产生:

有关详细信息,请参阅 Using Swift with Cocoa and Objective-C: Adopting Cocoa Patterns 的 "Key-Value Observing" 部分。