Reputation: 1820
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.
Upvotes: 1
Views: 798
Reputation: 437702
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.
Upvotes: 1