Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to observe individual array element changes (update) with Swift and KVO?

Do I need to subscribe/unsubscribe to individual elements of an array?

I want to update each table view cell individually to reflect changes in the backing array. By changes I mean not append/remove operations but update of the properties of the objects of the array. I hope I was able to explain what I want to achieve. Thanks

like image 584
mra214 Avatar asked May 29 '16 08:05

mra214


3 Answers

To use KVO, declare the model object with dynamic properties:

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

Then, let the cell process the KVO. First, I'd have a protocol by which the cell can inform the table view that it needs to be reloaded:

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

And the table view controller can conform to this CustomCellDelegate protocol and reload the cell when informed it needs to:

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

So, and then define cell to setup and handle KVO. In Swift 4 and iOS 11, you can use the closure-based observe method with the new strongly typed keys:

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

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

}

And now the cellForRowAtIndexPath can just set the delegate and object properties:

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
}

In my opinion, this KVO mechanism is better than the Swift observer mechanism because the model object doesn't need to know (nor should it) anything about the observer. It just needs to indicate that it supports dynamic dispatch, and that's it.

For Swift 2 rendition of the above, see previous revision of this answer.

like image 140
Rob Avatar answered Nov 14 '22 07:11

Rob


You could use setter observer on stored variables in the backing array.

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

when you change the value in array the willSet and didSet is executed, and you can call a function on the cell doing the update you want.

like image 44
Ivan C Myrvold Avatar answered Nov 14 '22 06:11

Ivan C Myrvold


You could use a delegate to handle communication between the individual cell:s data (say, elements of your array) and the owner of these cells (i.e., owner of the array). Changes in the "backing array" can be propagated to the array owned using delegate callbacks with appropriate information.

E.g.:

/* 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
    }
}

Example usage:

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."
like image 1
dfrib Avatar answered Nov 14 '22 06:11

dfrib