Swift MVC架构下模型变更观测的更优方案咨询(排除RxSwift)
嘿,这个场景我太熟悉了——用NotificationCenter监听模型变更确实容易踩这种「属性更新顺序受限、只能盯单个属性」的坑。给你几个纯Swift原生的替代方案,都不用RxSwift,能完美解决你的问题:
Combine是苹果官方推出的响应式编程框架,完全原生,用它来监听模型变更非常优雅。你只需要让模型遵循ObservableObject协议,并用@Published标记需要监听的属性,就能自动发送变更通知:
import Combine class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var gender: String init(name: String, age: Int, gender: String) { self.name = name self.age = age self.gender = gender } }
然后在视图控制器里,我们可以订阅模型的变更,自动更新UI:
class ViewController: UIViewController { let person = Person(name: "Homer", age: 44, gender: "male") // 用来存储订阅,防止提前释放 private var cancellables = Set<AnyCancellable>() @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() updateLabels() // 订阅模型的所有变更,只要任意属性变化就更新UI person.objectWillChange .receive(on: DispatchQueue.main) // 确保在主线程更新UI .sink { [weak self] in self?.updateLabels() } .store(in: &cancellables) } @IBAction func updatePerson(_ sender: Any) { guard let name = nameField.text, let ageText = ageField.text, let gender = genderField.text, let ageNumber = Int(ageText) else { return } person.name = name person.age = ageNumber person.gender = gender // 不用管属性设置顺序,任意属性变化都会触发UI更新 } func updateLabels() { nameLabel.text = person.name ageLabel.text = String(person.age) genderLabel.text = person.gender } }
这个方案的好处是代码简洁,自动处理线程切换,而且不管哪个属性变化,都会触发UI更新,完全解决了你原来的顺序问题。
如果你的项目需要支持iOS 13之前的系统,自定义闭包是个轻量且灵活的选择。我们可以在模型里定义一个回调闭包,每当属性变化时就调用它:
class Person { var name: String { didSet { onChange?() } } var age: Int { didSet { onChange?() } } var gender: String { didSet { onChange?() } } // 定义变更回调闭包 var onChange: (() -> Void)? init(name: String, age: Int, gender: String) { self.name = name self.age = age self.gender = gender } }
然后在视图控制器里,我们只需要给模型的onChange赋值,就能监听所有属性的变化:
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() updateLabels() // 设置回调,注意用weak self避免循环引用 person.onChange = { [weak self] in DispatchQueue.main.async { self?.updateLabels() } } } @IBAction func updatePerson(_ sender: Any) { guard let name = nameField.text, let ageText = ageField.text, let gender = genderField.text, let ageNumber = Int(ageText) else { return } person.name = name person.age = ageNumber person.gender = gender // 任意属性变化都会触发回调,不用在意顺序 } func updateLabels() { nameLabel.text = person.name ageLabel.text = String(person.age) genderLabel.text = person.gender } }
这个方案没有系统版本限制,代码也很直观,唯一需要注意的是用weak self避免循环引用,以及确保在主线程更新UI(如果属性可能在后台线程修改的话)。
KVO是Objective-C时代就有的技术,Swift里也能使用,但需要模型类继承自NSObject,并且属性标记为@objc dynamic:
class Person: NSObject { @objc dynamic var name: String @objc dynamic var age: Int @objc dynamic var gender: String init(name: String, age: Int, gender: String) { self.name = name self.age = age self.gender = gender super.init() } }
然后在视图控制器里添加观察者,监听每个属性的变化:
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() updateLabels() // 给每个属性添加观察者 person.addObserver(self, forKeyPath: #keyPath(Person.name), options: [.new], context: nil) person.addObserver(self, forKeyPath: #keyPath(Person.age), options: [.new], context: nil) person.addObserver(self, forKeyPath: #keyPath(Person.gender), options: [.new], context: nil) } // 处理属性变化的回调 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if object === person { DispatchQueue.main.async { self.updateLabels() } } } @IBAction func updatePerson(_ sender: Any) { guard let name = nameField.text, let ageText = ageField.text, let gender = genderField.text, let ageNumber = Int(ageText) else { return } person.name = name person.age = ageNumber person.gender = gender } func updateLabels() { nameLabel.text = person.name ageLabel.text = String(person.age) genderLabel.text = person.gender } // 记得在销毁时移除观察者,避免内存泄漏 deinit { person.removeObserver(self, forKeyPath: #keyPath(Person.name)) person.removeObserver(self, forKeyPath: #keyPath(Person.age)) person.removeObserver(self, forKeyPath: #keyPath(Person.gender)) } }
KVO的兼容性很好,但缺点是代码相对繁琐,需要手动管理观察者的添加和移除,而且模型必须继承NSObject,适合老项目维护的场景。
总结一下:如果你的项目支持iOS13+,Combine是最优选择,代码简洁且原生支持;如果需要兼容旧系统,自定义闭包是最轻便的方案;KVO则适合必须使用Objective-C生态的场景。这三个方案都能解决你原来的「属性顺序受限、只能监听单个属性」的问题。
内容的提问来源于stack exchange,提问作者wigging

