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

技术疑问:MVVM中如何通过ViewModel属性的willSet/didSet正确更新UI?

MVVM模式下属性更新同步UI的正确实现方案

问题说明

已知MVVM规范中ViewModel直接操作UI元素是错误实践,但不清楚属性更新时该如何同步更新UI(SwiftUI中可通过@Published轻松实现)。当前错误实现代码如下:

var exchangeRate: Double = 1.0 {
    willSet {
        guard let doubleFormattedValue = Double.format(newValue, numberStyle: .decimal, decimalPlaces: 6) else { return }
        DispatchQueue.main.async {
            self.rateLabel.text = String(doubleFormattedValue) + " " + self.fromCurrencyTextField.currency.rawValue
        }
    }
}

两种可行解决方案(针对UIKit场景)

1. 基于Combine框架的@Published绑定(贴近SwiftUI思路)

ViewModel专注处理数据与业务逻辑,用@Published标记需要同步的属性,ViewController订阅属性变化并更新UI:

ViewModel代码:

import Combine

class ExchangeViewModel {
    @Published var exchangeRate: Double = 1.0
    let fromCurrency: Currency // 假设Currency为自定义货币枚举
    
    // 封装格式化逻辑,直接返回可用于UI的字符串
    var formattedExchangeRate: String {
        guard let formattedValue = Double.format(exchangeRate, numberStyle: .decimal, decimalPlaces: 6) else {
            return "1.000000 \(fromCurrency.rawValue)"
        }
        return "\(formattedValue) \(fromCurrency.rawValue)"
    }
    
    init(fromCurrency: Currency) {
        self.fromCurrency = fromCurrency
    }
}

ViewController代码:

import UIKit
import Combine

class ExchangeViewController: UIViewController {
    @IBOutlet weak var rateLabel: UILabel!
    @IBOutlet weak var fromCurrencyTextField: CurrencyTextField! // 自定义货币输入框
    private var viewModel: ExchangeViewModel!
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = ExchangeViewModel(fromCurrency: fromCurrencyTextField.currency)
        
        // 订阅属性变化,自动同步UI
        viewModel.$exchangeRate
            .map { [weak viewModel] _ in
                viewModel?.formattedExchangeRate ?? ""
            }
            .receive(on: DispatchQueue.main)
            .assign(to: \.text, on: rateLabel)
            .store(in: &cancellables)
    }
}

2. 闭包回调方案(无需Combine)

在ViewModel中定义回调闭包,属性更新时触发闭包,ViewController实现闭包完成UI更新:

ViewModel代码:

class ExchangeViewModel {
    var exchangeRate: Double = 1.0 {
        didSet {
            // 属性更新时触发回调
            updateUIHandler?(formattedExchangeRate)
        }
    }
    let fromCurrency: Currency
    // 定义UI更新回调
    var updateUIHandler: ((String) -> Void)?
    
    var formattedExchangeRate: String {
        guard let formattedValue = Double.format(exchangeRate, numberStyle: .decimal, decimalPlaces: 6) else {
            return "1.000000 \(fromCurrency.rawValue)"
        }
        return "\(formattedValue) \(fromCurrency.rawValue)"
    }
    
    init(fromCurrency: Currency) {
        self.fromCurrency = fromCurrency
    }
}

ViewController代码:

class ExchangeViewController: UIViewController {
    @IBOutlet weak var rateLabel: UILabel!
    @IBOutlet weak var fromCurrencyTextField: CurrencyTextField!
    private var viewModel: ExchangeViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel = ExchangeViewModel(fromCurrency: fromCurrencyTextField.currency)
        
        // 实现回调,更新UI
        viewModel.updateUIHandler = { [weak self] formattedText in
            DispatchQueue.main.async {
                self?.rateLabel.text = formattedText
            }
        }
    }
}

核心原则

ViewModel只负责数据处理、业务逻辑,绝不持有任何UI元素引用;UI更新完全由ViewController负责,通过数据绑定或回调实现ViewModel到View的单向通信。

内容的提问来源于stack exchange,提问作者Ace Green

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 19:05:29