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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:10:22