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

Swift MVC转MVVM:didSelectItemAt代码迁移错误解决方法

Swift MVC转MVVM:解决didSelectItemAt迁移中的作用域错误

问题根源

  • ViewModel层不能引入UIKit,也无权访问ViewController中的UI控件(比如categoryCollectionView),你直接在VM里写collectionView == categoryCollectionView必然报错——这些控件仅属于VC的作用域。
  • MVVM职责边界:ViewModel负责数据处理、业务逻辑,ViewController负责UI渲染、用户交互响应、导航跳转,你把导航逻辑放到VM里违反了这个边界。

解决方案

我们通过「VM定义交互事件,VC响应事件」的方式实现解耦,同时把所有数据迁移到VM,VC只做UI相关工作。

步骤1:重构ViewModel,明确职责

import Foundation

// 定义VM需要通知VC的交互事件
enum BreedsVMAction {
    case showCategoryDetail(DogCategory)
    case showBreedDetail(Breed)
}

// 标识不同的列表类型,避免VM依赖UI控件
enum BreedsListType {
    case category
    case popular
    case special
}

protocol BreedsVMInterface {
    var view: BreedsViewControllerInterface? { get set }
    var actionHandler: ((BreedsVMAction) -> Void)? { get set } // 事件回调
    
    func viewDidLoad()
    func didSelectItem(from listType: BreedsListType, at indexPath: IndexPath)
}

final class BreedsVM {
    weak var view: BreedsViewControllerInterface?
    var actionHandler: ((BreedsVMAction) -> Void)?
    
    // 所有数据统一由VM管理
    var categories: [DogCategory] = []
    var populars: [Breed] = []
    var downCategories:[Breed] = []
    
    private let networkService = NetworkService.shared
}

extension BreedsVM: BreedsVMInterface {
    func didSelectItem(from listType: BreedsListType, at indexPath: IndexPath) {
        // 仅处理数据逻辑,通过回调通知VC执行导航
        switch listType {
        case .category:
            guard indexPath.row < categories.count else { return }
            actionHandler?(.showCategoryDetail(categories[indexPath.row]))
        case .popular:
            guard indexPath.row < populars.count else { return }
            actionHandler?(.showBreedDetail(populars[indexPath.row]))
        case .special:
            guard indexPath.row < downCategories.count else { return }
            actionHandler?(.showBreedDetail(downCategories[indexPath.row]))
        }
    }
    
    func viewDidLoad() {
        fetchData()
    }
    
    // 网络请求逻辑迁移到VM
    private func fetchData() {
        view?.showLoading()
        networkService.fetchAllCategories { [weak self] result in
            guard let self = self else { return }
            self.view?.hideLoading()
            
            switch result {
            case .success(let allBreed):
                self.categories = allBreed.categories ?? []
                self.populars = allBreed.populars ?? []
                self.downCategories = allBreed.downCategories ?? []
                self.view?.reloadAllCollections()
            case .failure(let error):
                self.view?.showError(error.localizedDescription)
            }
        }
    }
}

步骤2:更新ViewController,响应VM事件

import UIKit
import ProgressHUD

protocol BreedsViewControllerInterface: AnyObject {
    func showLoading()
    func hideLoading()
    func reloadAllCollections()
    func showError(_ message: String)
}

final class BreedsViewController: UIViewController {
    
    @IBOutlet weak var categoryCollectionView: UICollectionView!
    @IBOutlet weak var popularCollectionView: UICollectionView!
    @IBOutlet weak var specialsCollectionView: UICollectionView!
    
    private lazy var viewModel = BreedsVM()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.view = self
        // 绑定VM事件回调,处理导航
        viewModel.actionHandler = { [weak self] action in
            self?.handleVMAction(action)
        }
        viewModel.viewDidLoad()
        registerCell()
    }
    
    private func registerCell() {
        categoryCollectionView.register(UINib(nibName: CategoryCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: CategoryCollectionViewCell.identifier)
        popularCollectionView.register(UINib(nibName: DogPortraitCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogPortraitCollectionViewCell.identifier)
        specialsCollectionView.register(UINib(nibName: DogLandscapeCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogLandscapeCollectionViewCell.identifier)
    }
    
    // 负责导航跳转,完全属于VC职责
    private func handleVMAction(_ action: BreedsVMAction) {
        switch action {
        case .showCategoryDetail(let category):
            let controller = ListDogsViewController.instantiate()
            controller.category = category
            navigationController?.pushViewController(controller, animated: true)
        case .showBreedDetail(let breed):
            let controller = FavoriteDetailViewController.instantiate()
            controller.breed = breed
            navigationController?.pushViewController(controller, animated: true)
        }
    }
}

extension BreedsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // 从VM获取数据数量
        switch collectionView {
        case categoryCollectionView:
            return viewModel.categories.count
        case popularCollectionView:
            return viewModel.populars.count
        case specialsCollectionView:
            return viewModel.downCategories.count
        default: return 0
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // 从VM获取数据渲染Cell
        switch collectionView {
        case categoryCollectionView:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCollectionViewCell.identifier, for: indexPath) as! CategoryCollectionViewCell
            cell.setup(category: viewModel.categories[indexPath.row])
            return cell
        case popularCollectionView:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogPortraitCollectionViewCell.identifier, for: indexPath) as! DogPortraitCollectionViewCell
            cell.setup(breed: viewModel.populars[indexPath.row])
            return cell
        case specialsCollectionView:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogLandscapeCollectionViewCell.identifier, for: indexPath) as! DogLandscapeCollectionViewCell
            cell.setup(breed: viewModel.downCategories[indexPath.row])
            return cell
        default: return UICollectionViewCell()
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // 判断列表类型,传递给VM
        let listType: BreedsListType
        switch collectionView {
        case categoryCollectionView:
            listType = .category
        case popularCollectionView:
            listType = .popular
        case specialsCollectionView:
            listType = .special
        default: return
        }
        viewModel.didSelectItem(from: listType, at: indexPath)
    }
}

extension BreedsViewController: BreedsViewControllerInterface {
    func showLoading() {
        ProgressHUD.show()
    }
    
    func hideLoading() {
        ProgressHUD.dismiss()
    }
    
    func reloadAllCollections() {
        categoryCollectionView.reloadData()
        popularCollectionView.reloadData()
        specialsCollectionView.reloadData()
    }
    
    func showError(_ message: String) {
        ProgressHUD.showError(message)
    }
}

关键改进点

  1. 职责彻底分离:VM只处理数据和业务逻辑,导航、UI操作完全交给VC,VM中无任何UIKit依赖。
  2. 事件驱动交互:通过BreedsVMAction枚举和回调,VM仅传递操作结果,VC负责执行具体动作,解耦两者依赖。
  3. 数据统一管理:所有数据源移到VM,VC不再持有数据,避免数据不一致问题。
  4. 网络逻辑迁移:网络请求从VC移到VM,VC仅负责展示状态和刷新UI,符合MVVM分层逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 22:15:33