Search code examples
iosswiftmodel-view-controllerkey-value-observing

Swift MVC: Having viewController observe Model property, updating the view when the property changes


The main class of my model is Concentration with a property called flipCount

class Concentration {
    @objc var flipCount = 0

In My ViewController I have an @IBOutlet to a UILabel called flipCountLabel

class ViewController: UIViewController {
    lazy var game = Concentration(numberOfPairsOfCards: self.cardButtons.count / 2)
    @IBOutlet weak var flipCountLabel: UILabel!
...

I want to update the text in flipCountLabel when flipCount changes. I am trying to do this by placing a call to the observe method of UIViewController in my ViewController's viewDidLoad method. There are a few observe methods. I don't know which one is right and cannot find an example for how to what to fill in the variables with.

I have tried to use self.observe like

    self.observe(KeyPath<Concentration,Int>(game, "flipCount"), changeHandler: {
        self.flipCountLabel.text = "Flip Count: \(flipCount)"
    })

I am trying to update the ViewControllers flipCountLabel.text every time game.flipCount is updated. I think this is kvo?


Solution

  • The observe method is the way to go here, be it with some changes to your code.

    Change your Concentration class to the following:

    class Concentration: NSObject {
        @objc dynamic var flipCount = 0
    }
    

    In order for KVO to work in Swift, properties need to be marked with both @objc and dynamic. The class needs to inherit from NSObject to prevent runtime errors.

    Your ViewController class should look something like this:

    class ViewController: UIViewController {
        @objc lazy var game = Concentration()
        @IBOutlet weak var flipCountLabel: UILabel!
    
        private var observation: NSKeyValueObservation?
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            self.observation = self.observe(\.game.flipCount) { _, _ in
                self.flipCountLabel.text = "\(self.game.flipCount)"
            }
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
    
            self.observation = nil
        }
    }
    

    The game property needs to be marked @objc (again to prevent runtime errors). Furthermore, the documentation of the observe method states

    when the returned NSKeyValueObservation is deinited or invalidated, it will stop observing

    This is why you need to keep a strong reference to the value returned by the observe method.

    Also, consider using the KeyPath notation I used here. If you make a typo, it will fail when compiling, instead of at runtime.

    I hope this helps you out!