You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift MVC架构下模型变更观测的更优方案咨询(排除RxSwift)

嘿,这个场景我太熟悉了——用NotificationCenter监听模型变更确实容易踩这种「属性更新顺序受限、只能盯单个属性」的坑。给你几个纯Swift原生的替代方案,都不用RxSwift,能完美解决你的问题:

方案1:Combine框架(iOS 13+)

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更新,完全解决了你原来的顺序问题。

方案2:自定义闭包回调(兼容iOS 13以下)

如果你的项目需要支持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(如果属性可能在后台线程修改的话)。

方案3:Key-Value Observing(KVO)

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:44:07