技术疑问: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
相关产品推荐
相关产品推荐

