Swift中MVVM模式下ViewModel委托的正确设置方式
Swift MVVM中ViewModel设置委托的正确姿势
这个问题我之前做项目时也踩过一模一样的坑!你的思路完全是对的,咱们一步步把这个问题捋清楚:
为什么你当前的做法会出问题?
你现在是先创建DashboardViewModel,再把它传入DashboardViewController的构造方法。但ViewModel的init里直接调用了loadItems——这时候ViewController还没来得及把自己设置为ViewModel的委托,reloadDelegate自然是nil,后续的回调根本触发不了。
正确的解决方案:把数据加载逻辑从ViewModel初始化中剥离
你的想法很对:把loadItems声明在DashboardViewModelType协议里,然后由ViewController在合适的生命周期时机调用,确保委托已经设置完成。
第一步:定义ViewModel协议和委托协议
先把ViewModel的能力和委托回调都抽象成协议,保证代码的可测试性和扩展性:
// 定义ViewModel的核心能力协议 protocol DashboardViewModelType: AnyObject { var reloadDelegate: DashboardViewModelReloadDelegate? { get set } func loadItems() // 把数据加载方法放到协议里 } // 定义ViewModel的委托协议,用于回调UI更新 protocol DashboardViewModelReloadDelegate: AnyObject { func didFinishLoadingItems() // 可以根据需求添加更多回调,比如加载失败、加载中 func didFailLoading(with error: Error) }
第二步:实现ViewModel类
ViewModel的init只做必要的属性初始化,绝对不要在init里调用数据加载方法:
class DashboardViewModel: DashboardViewModelType { weak var reloadDelegate: DashboardViewModelReloadDelegate? private var items: [Item] = [] // 假设的数据源 init() { // 这里只初始化基础属性,不碰数据加载 } func loadItems() { // 模拟异步加载数据(比如网络请求、本地数据库查询) DispatchQueue.global().async { [weak self] in // 实际的加载逻辑:比如请求API获取items let fetchedItems = self?.fetchItemsFromAPI() ?? [] self?.items = fetchedItems // 回到主线程回调UI DispatchQueue.main.async { self?.reloadDelegate?.didFinishLoadingItems() } } } // 模拟API请求 private func fetchItemsFromAPI() -> [Item] { // 这里写实际的网络请求逻辑 return [Item(name: "Item 1"), Item(name: "Item 2")] } }
敲黑板:委托属性一定要用
weak修饰!因为ViewController持有ViewModel,ViewModel的委托是ViewController,用weak可以避免循环引用导致的内存泄漏。
第三步:在ViewController中正确关联ViewModel并触发加载
在ViewController的构造方法中传入ViewModel,先设置委托,再在合适的时机调用loadItems:
class DashboardViewController: UIViewController, DashboardViewModelReloadDelegate { private let viewModel: DashboardViewModelType private let tableView = UITableView() // 假设的UI组件 // 构造方法传入ViewModel,依赖注入更利于测试 init(viewModel: DashboardViewModelType) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) // 这里可以先设置委托,因为ViewController已经初始化完成 viewModel.reloadDelegate = self } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupTableView() // 先初始化UI组件 viewModel.loadItems() // 再触发数据加载,此时委托和UI都准备好了 } private func setupTableView() { // 配置tableView的布局、注册cell等逻辑 view.addSubview(tableView) tableView.frame = view.bounds tableView.dataSource = self } // 实现委托回调,更新UI func didFinishLoadingItems() { tableView.reloadData() } func didFailLoading(with error: Error) { // 显示错误提示,比如Alert let alert = UIAlertController(title: "加载失败", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default)) present(alert, animated: true) } } // 实现UITableViewDataSource extension DashboardViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // 假设ViewModel提供获取items的方法,这里可以补充到协议里 guard let viewModel = viewModel as? DashboardViewModel else { return 0 } return viewModel.items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) guard let viewModel = viewModel as? DashboardViewModel else { return cell } cell.textLabel?.text = viewModel.items[indexPath.row].name return cell } }
额外注意点
- 为什么不在ViewController的init里直接调用loadItems?
因为此时ViewController的view还没加载,UI组件(比如tableView)可能还未完成初始化,直接调用加载的话,回调里更新UI会出问题。viewDidLoad是UI组件初始化完成后的第一个生命周期方法,在这里调用最稳妥。 - 如果需要提前加载数据怎么办?
比如有些场景需要在ViewController初始化后立刻加载,可以在init设置完委托后调用loadItems,但要确保在委托回调里判断UI组件是否已初始化(比如用if let tableView = tableView),避免崩溃。
内容的提问来源于stack exchange,提问作者mikro098
相关产品推荐
相关产品推荐

