Архитектура Swift MVC с наблюдателем модели

Используя подход MVC для разработки приложений для iOS, я хотел бы наблюдать за изменениями в модели, публикуя их в файле NotificationCenter. В моем примере модель Person.swift:

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

Контроллер представления, который наблюдает за моделью, показан ниже:

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

Пример приложения работает следующим образом:

  1. Введите имя, возраст и пол в текстовые поля
  2. Нажмите кнопку updatePerson, чтобы обновить модель из значений текстового поля.
  3. Когда модель обновляется, наблюдатель уведомлений вызывает функцию updateLabels для обновления пользовательского интерфейса.

Этот подход требует, чтобы кнопка person.name была установлена ​​последней, в противном случае кнопку updatePerson необходимо нажать дважды, чтобы обновить весь пользовательский интерфейс. И поскольку я наблюдаю только за одним свойством, уведомление не представляет весь класс. Есть ли лучший способ наблюдать за изменениями моделей (класса или структуры) в Swift?

Примечание. Я не заинтересован в использовании RxSwift для этого примера.


person wigging    schedule 23.05.2018    source источник


Ответы (2)


Это скорее демпинговый комментарий, чем полноценный ответ. Короче говоря, вам следует использовать KVO, а не NotificationCenter. Процесс привязки становится значительно проще в Swift4.

Что такое KVO: см. здесь и здесь. Для некоторых примеров, ориентированных на MVVM, вы можете увидеть здесь и здесь. И не позволяйте MVVM сбить вас с толку. Это просто MVC с привязками, которые вы пытаетесь сделать точно так же + перемещаете логику представления на другой уровень.

Простой пример KVO в Swift 4 будет выглядеть так:

@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

Вы также можете создать свой собственный тип Observable, как показано ниже:

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

Затем, чтобы создать наблюдаемое свойство, вы должны сделать:

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

Приведенный выше класс (Observable) скопирован и вставлен из книги шаблонов Swift Designs

person Honey    schedule 23.05.2018

Чтобы просто визуализировать картину, вы должны осознавать тот факт, что вы наблюдаете только name изменение. Поэтому нет смысла обновлять все остальные свойства Person. Вы наблюдаете name изменение, и оно соответствующим образом обновляется, не говоря уже о других.

Так что это не идеальное предположение, что age и gender могли быть изменены в процессе изменения name. При этом вам следует рассмотреть возможность наблюдения за всеми свойствами одно за другим, по-разному связывать действия и изменять только компонент пользовательского интерфейса, который сопоставлен с этим конкретным свойством.

Что-то вроде этого:

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
}
person nayem    schedule 23.05.2018
comment
Я хотел бы избежать применения наблюдателей к каждому свойству, как вы предлагаете. Это невозможно в более крупном приложении, где может быть гораздо больше свойств. Я сделал свой пример простым ради вопроса, но на самом деле приложение намного сложнее. - person wigging; 23.05.2018