1

Using the MVC approach for iOS app development, I would like to observe changes to the model by posting to the NotificationCenter. For my example, the Person.swift model is:

class Person {

    static let nameDidChange = Notification.Name("nameDidChange")

    var name: String {
        didSet {
            NotificationCenter.default.post(name: Person.nameDidChange, object: self)
        }
    }

    var age: Int
    var gender: String

    init(name: String, age: Int, gender: String) {
        self.name = name
        self.age = age
        self.gender = gender
    }
}

The view controller that observes the model is shown below:

class ViewController: UIViewController {

    let person = Person(name: "Homer", age: 44, gender: "male")

    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var ageLabel: UILabel!
    @IBOutlet weak var genderLabel: UILabel!
    @IBOutlet weak var nameField: UITextField!
    @IBOutlet weak var ageField: UITextField!
    @IBOutlet weak var genderField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = person.name
        ageLabel.text = String(person.age)
        genderLabel.text = person.gender

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(self.updateLabels),
                                               name: Person.nameDidChange, object: nil)
    }

    @IBAction func updatePerson(_ sender: Any) {
        guard let name = nameField.text, let age = ageField.text, let gender = genderField.text else { return }
        guard let ageNumber = Int(age) else { return }
        person.name = name
        person.age = ageNumber
        person.gender = gender
    }

    @objc func updateLabels() {
        nameLabel.text = person.name
        ageLabel.text = String(person.age)
        genderLabel.text = person.gender
    }
}

The example app works as follows:

  1. Enter a name, age, and gender in the text fields
  2. Press the updatePerson button to update the model from the text field values
  3. When the model is updated, the notification observer calls the updateLabels function to update the user interface.

This approach requires the person.name to be set last otherwise the updatePerson button must be pressed twice to update the entire user interface. And since I'm only observing one property, the notification does not represent the entire class. Is there a better way to observe changes of models (a class or struct) in Swift?

Note - I am not interested in using RxSwift for this example.

wigging
  • 8,492
  • 12
  • 75
  • 117

2 Answers2

1

This is more of a dumping comment than a fulfilling answer. But long story short KVO is the feature you should be using, not NotificationCenter. The binding process becomes significantly more simple in Swift4

As for what KVO is: See here and here. For some examples which are MVVM focused you can see here and here. And don't let the MVVM sway you away. It's just MVC with bindings which you are trying to do the exact same thing + moving the presentation logic to a different layer.

A simple KVO example in Swift 4 would look like this:

@objcMembers class Foo: NSObject {
    dynamic var string: String
    override init() {
        string = "hotdog"
        super.init()
    }
}

let foo = Foo()

// Here it is, kvo in 2 lines of code!
let observation = foo.observe(\.string) { (foo, change) in
    print("new foo.string: \(foo.string)")
}

foo.string = "not hotdog"
// new foo.string: not hotdog

You can also create your own Observable type like below:

class Observable<ObservedType>{

    private var _value: ObservedType?

    init(value: ObservedType) {
        _value = value
    }

    var valueChanged : ((ObservedType?) -> ())?


    public var value: ObservedType? {
        get{
            return _value // The trick is that the public value is reading from the private value...
        }

        set{
            _value = newValue
            valueChanged?(_value)
        }
    }

    func bindingChanged(to newValue : ObservedType){
        _value = newValue 
        print("value is now \(newValue)")
    }
}

Then to create an observable property you'd do:

class User {
    //    var name : String <-- you normally do this, but not when you're creating as such
    var name : Observable<String>

    init (name: Observable<String>){
        self.name = name            
    }
}

The class above (Observable) is copied and pasted from Swift Designs patterns book

mfaani
  • 33,269
  • 19
  • 164
  • 293
  • How is this different than using the `NotificationCenter` observer? – wigging May 23 '18 at 13:46
  • with KVO you don't have to use `didSet`, nor you run into the issue you just mentioned. Simply put you just observe any property/keypath from any class **without** any setup *on* the class/property you want to observe. And since Swift4, it has became more convenient. Meaning unlike NotificationCenter you don't have to post...then addobserver...then have a selector...you just single callback as simple as [this post](http://skyefreeman.io/programming/2017/06/28/kvo-in-ios11.html) – mfaani May 23 '18 at 14:16
  • The code example in the link in your last comment is much more straight-forward compared to the example you gave in your answer. Please revise your answer using approach outlined in the example [here](http://skyefreeman.io/programming/2017/06/28/kvo-in-ios11.html). – wigging May 24 '18 at 03:01
0

To simply visualize the picture, you should be aware of the fact that you are observing only the name change. So it doesn't make sense to update all of the other properties of Person. You are observing name change and it's being updated accordingly, let alone others.

So it's not an ideal assumption that age and gender might have been changed in the process of changing name. Being said that, you should consider observing all of the properties one by one and bind actions differently and modify only the UI component that is mapped to that specific property.

Something like this:

override func viewDidLoad() {
    ...
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.updateName),
                                           name: Person.nameDidChange, object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.updateAge),
                                           name: Person.ageDidChange, object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.updateGender),
                                           name: Person.genderDidChange, object: nil)
    ...
}

@objc func updateName() {
    nameLabel.text = person.name
}
@objc func updateAge() {
    ageLabel.text = String(person.age)
}
@objc func updateGender() {
    genderLabel.text = person.gender
}
nayem
  • 7,285
  • 1
  • 33
  • 51
  • I would like to avoid applying observers to every property as you suggest. This is not feasible in a larger application where there could be many more properties. I kept my example simple for the sake of the question but in reality the app is much more complex. – wigging May 23 '18 at 03:20