Using KVO within a tableview cell to track changes to specific properties of a class instance in Swift 3

I’m trying to use KVO to track changes to the properties of my Account object, the important part of which looks like this:

class Account: NSObject {
    var battleTagLabel: String!
    dynamic var onlineStatusIcon: String!
    dynamic var currentGameIcon: String!
    dynamic var currentStatusLabel: String!

I want to be notified within my tableview cell when those three properties change in value. My tableview cell class:

import Foundation
import UIKit

private var observerContext = 0

class FriendAccountCell: UITableViewCell {

    @IBOutlet weak var onlineStatusIcon: UIImageView!
    @IBOutlet weak var battleTag: UILabel!
    @IBOutlet weak var currentGameIcon: UIImageView!
    @IBOutlet weak var currentStatusLabel: UILabel!

    weak var tableView: UITableView!
    weak var delegate: CustomCellDelegate?
    var onlineStatusIconFlag = false
    var currentStatusLabelFlag = false
    var currentGameIconFlag = false

    var account: Account? {

        willSet {
            if onlineStatusIconFlag {
                print("onlineStatusIconFlag: /(onlineStatusIconFlag)")
                if newValue?.onlineStatusIcon != account?.onlineStatusIcon && account?.onlineStatusIcon != nil {
                    self.account?.removeObserver(self, forKeyPath: #keyPath(onlineStatusIcon))
                    onlineStatusIconFlag = false

                }
            }
            if currentStatusLabelFlag {
                if newValue?.currentStatusLabel != account?.currentStatusLabel && account?.currentStatusLabel != nil {
                    account?.removeObserver(self, forKeyPath: #keyPath(currentStatusLabel))
                    currentStatusLabelFlag = false
                }
            }

            if currentGameIconFlag {
                if newValue?.currentGameIcon != account?.currentGameIcon && account?.currentGameIcon != nil {
                    account?.removeObserver(self, forKeyPath: #keyPath(currentGameIcon))
                    currentGameIconFlag = false

                }
            }
        }

        didSet {
            if oldValue?.onlineStatusIcon != account?.onlineStatusIcon {
                if account?.onlineStatusIcon == "onine" {
                    self.onlineStatusIcon.image = UIImage(named: "20pxButtonGreen")
                } else if account?.onlineStatusIcon == "idle" {
                    self.onlineStatusIcon.image = UIImage(named: "20pxButtonYellow")
                } else if account?.onlineStatusIcon == "busy" {
                    self.onlineStatusIcon.image = UIImage(named: "20pxButtonRed")
                } else {
                    self.onlineStatusIcon.image = UIImage(named: "20pxButtonBlack")
                }
                account?.addObserver(self, forKeyPath: #keyPath(onlineStatusIcon), context: &observerContext)
                onlineStatusIconFlag = true
            }

            if oldValue?.currentStatusLabel != account?.currentStatusLabel {
                self.currentStatusLabel?.text = account?.currentStatusLabel
                account?.addObserver(self, forKeyPath: #keyPath(currentStatusLabel), context: &observerContext)
                currentStatusLabelFlag = true
            }

            if oldValue?.currentGameIcon != account?.currentGameIcon {
                if let currentGame = account?.currentGameIcon {
                    switch currentGame {
                    case "overwatch":
                        self.currentGameIcon.image = UIImage(named: "logo-ow")
                    case "hearthstone":
                        self.currentGameIcon.image = UIImage(named: "logo-hs")
                    case "worldOfWarcraft":
                        self.currentGameIcon.image = UIImage(named: "logo-wow")
                    case "diablo3":
                        self.currentGameIcon.image = UIImage(named: "logo-d3")
                    case "heroesOfTheStorm":
                        self.currentGameIcon.image = UIImage(named: "logo-heroes")
                    case "starCraft":
                        self.currentGameIcon.image = UIImage(named: "logo-sc")
                    case "starCraft2":
                        self.currentGameIcon.image = UIImage(named: "logo-sc2")
                    case "":
                        self.currentGameIcon.image = nil
                    default:
                        self.currentGameIcon.image = nil
                    }
                }
                account?.addObserver(self, forKeyPath: #keyPath(currentGameIcon), context: &observerContext)
                currentGameIconFlag = true
            }
        }

    }
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(cell: self)
}


deinit {
    print("deinit called")
    if onlineStatusIconFlag {
        account?.removeObserver(self, forKeyPath: #keyPath(onlineStatusIcon))
        onlineStatusIconFlag = false
    }
    if currentStatusLabelFlag {
        account?.removeObserver(self, forKeyPath: #keyPath(currentStatusLabel))
        currentStatusLabelFlag = false
    }
    if currentGameIconFlag {
        account?.removeObserver(self, forKeyPath: #keyPath(currentGameIcon))
        currentGameIconFlag = false
    }
}

And here is the relevant section of my tableview class:

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

    let identifier: String = "FriendAccountCell"
    var cell: FriendAccountCell
    if let friendCell = self.tableView.dequeueReusableCell(withIdentifier: identifier){
        cell = friendCell as! FriendAccountCell
    } else {
        cell = FriendAccountCell(style: .default, reuseIdentifier: identifier)
        cell.selectionStyle = .none
    }
    var filteredFriends = orderFriends(friendsArray: Array(MyAccountInfo.allFriends.values))

    cell.delegate = self
    cell.account = filteredFriends[indexPath.row]
    cell.battleTag.text = filteredFriends[indexPath.row].battleTagLabel
    cell.currentStatusLabel.text = filteredFriends[indexPath.row].currentStatusLabel

    return cell
}

(It’s not pasted above, but I also implement the delegate function in my tableview class to reload the specific cells.)

The changes to these specific properties happen quickly when the app first loads and all the most current data gets grabbed from the server. Afterward the changes happen more steadily and slowly.

Despite the flags and other strategies I’ve tried to properly track the addition and removal of observers, I’m still getting the “Cannot remove observer for the key path because it is not registered as an observer” error.

I’d suggest simplifying the add/remove observer logic. The current code is too complicated and offers too many paths where you might miss one or the other. So, just remove observers in willSet, and add observers in didSet:

var account: Account? {

    willSet {
        account?.removeObserver(self, forKeyPath: #keyPath(Account.onlineStatusIcon))
        account?.removeObserver(self, forKeyPath: #keyPath(Account.currentStatusLabel))
        account?.removeObserver(self, forKeyPath: #keyPath(Account.currentGameIcon))
    }

    didSet {
        account?.addObserver(self, forKeyPath: #keyPath(Account.onlineStatusIcon), context: &observerContext)
        account?.addObserver(self, forKeyPath: #keyPath(Account.currentStatusLabel), context: &observerContext)
        account?.addObserver(self, forKeyPath: #keyPath(Account.currentGameIcon), context: &observerContext)

        // do any additional logic here you want here
    }

}

deinit {
    account?.removeObserver(self, forKeyPath: #keyPath(Account.onlineStatusIcon))
    account?.removeObserver(self, forKeyPath: #keyPath(Account.currentStatusLabel))
    account?.removeObserver(self, forKeyPath: #keyPath(Account.currentGameIcon))
}

Also, if you set account in init, remember that willSet is not called then, so you’ll have to manually add the observers yourself in that one situation.