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) } }
关键改进点
- 职责彻底分离:VM只处理数据和业务逻辑,导航、UI操作完全交给VC,VM中无任何UIKit依赖。
- 事件驱动交互:通过
BreedsVMAction枚举和回调,VM仅传递操作结果,VC负责执行具体动作,解耦两者依赖。 - 数据统一管理:所有数据源移到VM,VC不再持有数据,避免数据不一致问题。
- 网络逻辑迁移:网络请求从VC移到VM,VC仅负责展示状态和刷新UI,符合MVVM分层逻辑。
内容的提问来源于stack exchange,提问作者LordGrim
相关产品推荐
相关产品推荐

