一、前言
如果你开发过移动端 App,那你肯定听说过 MVVM 和 VIPER。有观点说 MVVM 的扩展性不够好,也有观点说 VIPER 是个过度设计的产物。而这篇文章想说的是——它俩非常接近,甚至我们都没有必要去把它俩分开对待。
VIPER 不是 MVVM 的替代品,而是 MVVM 的自然延伸。
本文将通过一个名为 TopMovies 的电影列表 App,经历三次迭代,展示架构如何随需求变化而演化。每一步变化都对应一个真实场景:
| 迭代 | 场景 | 架构变化 |
|---|---|---|
| 阶段一 | 开发 iOS 版电影列表 | MVVM(View + ViewModel) |
| 阶段二 | 需要同时开发 macOS 版 | 引入 Presenter |
| 阶段三 | iOS 端 UI 重设计 + A/B 测试 | 引入 Router,形成完整 VIPER |
完整代码可在 GitHub 查看,直接用 Xcode 打开即可运行。
二、两种架构的核心
MVVM
- View 将用户行为传递给 ViewModel。
- ViewModel 处理这些行为并更新状态。
- ViewModel 通过数据绑定或 delegation 通知 View 更新。
VIPER
- View 将用户行为传递给 Presenter。
- Presenter 将行为分发给 Interactor(计算)或 Router(导航)。
- Interactor 处理后返回状态给 Presenter。
- Presenter 将状态转化为展示数据并更新 View。
- Router 封装导航逻辑,由 Presenter 触发。
核心区别:MVVM 的 UI 层只有一个 View 组件,而 VIPER 将 UI 层拆分成了三个:View、Presenter、Router。业务层两者基本一样。
三、项目结构总览
ArchitectureDemo/
├── AppMain.swift # @main 入口 (UIKit AppDelegate)
├── ContentView.swift # UITabBarController 主界面
├── Entities/
│ ├── Movie.swift # Entity + MoviePresentation + MovieService
│ ├── MovieTableViewCell.swift # 通用 Cell
│ └── MovieDetailViewController.swift # 通用详情页
├── Stage1_MVVM/
│ ├── MovieListViewModel.swift # 协议定义(博文原文)+ 具体实现
│ └── MovieListView_MVVM.swift # ViewController 实现
├── Stage2_Presenter/
│ ├── MovieListPresenter.swift # 协议定义(博文原文)+ 具体实现
│ └── MovieListView_Presenter.swift # ViewController 实现
└── Stage3_VIPER/
├── MovieListRouter.swift # Router 协议(博文原文)+ 两版实现
└── MovieListView_VIPER.swift # Presenter + ViewController 实现
四、基础层:Entity、展示模型与数据服务
这是贯穿三个阶段的共享基础代码:
// MARK: - Entity
struct Movie {
let id: String
let title: String
let year: Int
let rating: Double
let summary: String
}
// MARK: - MoviePresentation(展示数据)
struct MoviePresentation {
let id: String
let displayTitle: String // "肖申克的救赎 (1994)"
let ratingText: String // "⭐️ 9.7"
let summaryPreview: String
init(from movie: Movie) {
self.id = movie.id
self.displayTitle = "\(movie.title) (\(movie.year))"
self.ratingText = String(format: "⭐️ %.1f", movie.rating)
self.summaryPreview = String(movie.summary.prefix(80)) + "..."
}
}
// MARK: - Mock Service
final class MovieService {
func fetchMovies(completion: @escaping ([Movie]) -> Void) {
// 模拟网络请求,1.5 秒后返回数据
}
}
五、阶段一:MVVM
协议定义
protocol MovieListView: MovieListViewModelDelegate {
private var viewModel: MovieListViewModel
func updateWithMovies(_ movies: [Movie])
func didTapOnReload()
func didTapOnMovie(at index: Int)
func showDetailView(for movie: Movie)
}
protocol MovieListViewModelDelegate: class {
func viewModelDidUpdate(_ model: MovieListViewModel)
}
protocol MovieListViewModel {
weak var delegate: MovieListViewModelDelegate? { get set }
var movies: [Movie] { get }
func fetchMovies()
}
架构图
┌──────────┐ delegate ┌───────────────┐
│ View │ ◄──────────── │ ViewModel │
│ (UIKit) │ ── action ──► │ (业务逻辑) │
└──────────┘ └───────────────┘
数据流
- View 把自己作为 ViewModel 的 delegate
- 用户点击重载
- View 调用
viewModel.fetchMovies() - 数据获取成功后,ViewModel 通知 delegate(view)
- 调用
updateWithMovies并显示数据
具体实现
ViewModel 实现:
final class MovieListViewModelImpl: MovieListViewModel {
weak var delegate: MovieListViewModelDelegate?
private(set) var movies: [Movie] = []
private let service: MovieService
func fetchMovies() {
service.fetchMovies { [weak self] movies in
guard let self = self else { return }
self.movies = movies
self.delegate?.viewModelDidUpdate(self)
}
}
}
View 实现(UIViewController):
final class MovieListViewController_MVVM: UIViewController, MovieListViewModelDelegate {
private let viewModel: MovieListViewModel
override func viewDidLoad() {
super.viewDidLoad()
// 博文:View 把自己作为 view model 的 delegate
viewModel.delegate = self
}
// MovieListView 协议
func updateWithMovies(_ movies: [Movie]) { tableView.reloadData() }
func didTapOnReload() { viewModel.fetchMovies() }
func didTapOnMovie(at index: Int) {
showDetailView(for: viewModel.movies[index])
}
func showDetailView(for movie: Movie) {
// 导航逻辑直接在 View 中
present(nav, animated: true)
}
// MovieListViewModelDelegate
func viewModelDidUpdate(_ model: MovieListViewModel) {
updateWithMovies(model.movies)
}
}
优缺点
- ✅ 结构简单,上手快
- ✅ View 和 ViewModel 职责清晰
- ❌ 导航逻辑在 View 中,跨平台需重复实现
- ❌ 数据格式化在 View 中(
cell.configure(with: movie)需要 View 理解 Movie 结构)
六、阶段二:引入 Presenter
场景:需要实现 macOS 版本
当需要开发 macOS 版本时,View 类完全不同(UIViewController vs NSViewController),无法复用。但"数据 → 展示格式"的转化逻辑可以复用。
于是将转化逻辑抽离到 Presenter 中。
协议定义
protocol MovieListView: MovieListPresenterDelegate {
private var presenter: MovieListPresenter
func didTapOnReload()
func didTapOnMovie(at index: Int)
func showDetailView(for movie: Movie)
}
protocol MovieListPresenterDelegate {
func updateWithMoviePresentations(_ movies: [MoviePresentation])
}
protocol MovieListPresenter: MovieListViewModelDelegate {
private var viewModel: MovieListViewModel
func reload()
func presentation(from movie: Movie) -> MoviePresentation
}
架构图
┌──────────┐ delegate ┌───────────────┐ delegate ┌───────────────┐
│ View │ ◄────────── │ Presenter │ ◄────────── │ ViewModel │
│ (UIKit) │ ─ action ─► │ (展示逻辑) │ ─ fetch ──► │ (业务逻辑) │
└──────────┘ └───────────────┘ └───────────────┘
数据流
- View 把自己作为 Presenter 的 delegate
- Presenter 把自己作为 ViewModel 的 delegate
- 用户点击重载
- View 调用
presenter.reload() - Presenter 调用
viewModel.fetchMovies() - 数据获取成功后,ViewModel 通知 delegate(presenter)
- Presenter 将 Movie 转化为 MoviePresentation,通知 delegate(view)
- View 更新自己
具体实现
Presenter 实现:
final class MovieListPresenterImpl: MovieListPresenter {
let viewModel: MovieListViewModel
init(viewModel: MovieListViewModel = MovieListViewModelImpl()) {
self.viewModel = viewModel
viewModel.delegate = self // 博文:把自己作为 viewModel 的 delegate
}
func reload() { viewModel.fetchMovies() }
func presentation(from movie: Movie) -> MoviePresentation {
MoviePresentation(from: movie)
}
func viewModelDidUpdate(_ model: MovieListViewModel) {
// 转化数据并通知 View
let presentations = model.movies.map { presentation(from: $0) }
_onUpdate?(presentations)
}
}
关键变化
-
showDetailView仍在 View 协议中,导航逻辑未抽离 - View 消费
MoviePresentation而非原始Movie,不再关心数据格式 - 任何平台只需实现
MovieListView协议即可复用展示逻辑
七、阶段三:引入 Router,完整 VIPER
场景:iOS UI 重设计 + A/B 测试
苹果发布了新设计语言,需要实现两套 UI 并通过 A/B 测试控制。showDetailView 在两套 View 中实现完全相同,不应复制粘贴。
协议定义
// 引入 Router 协议
protocol MovieListRouter {
func showDetailView(for movie: Movie)
}
Router 应该放在哪里?放在 View 里也可以,但经验告诉我们不要频繁改 View。于是让 Presenter 持有 Router——用户行为发生时,Presenter 决定是调用 ViewModel 做计算还是调用 Router 做导航。
最终代码结构(100% VIPER)
protocol MovieListView: MovieListPresenterDelegate {
private var presenter: MovieListPresenter
func didTapOnReload()
func didTapOnMovie(at index: Int)
}
protocol MovieListPresenterDelegate {
func updateWithMoviePresentations(_ movies: [MoviePresentation])
}
protocol MovieListPresenter: MovieListViewModelDelegate {
private var router: MovieListRouter
private var viewModel: MovieListViewModel
func reload()
func presentation(from movie: Movie) -> MoviePresentation
}
protocol MovieListRouter {
func showDetailView(for movie: Movie)
}
注意:
showDetailView已从MovieListView协议中移除,导航完全交给 Router。
架构图
┌──────────┐ delegate ┌───────────────┐ delegate ┌───────────────┐
│ View │ ◄────────── │ Presenter │ ◄────────── │ Interactor │
│ (UIKit) │ ─ action ─► │ │ ─ fetch ──► │ (原ViewModel) │
└──────────┘ │ ┌─────────┐ │ └───────────────┘
│ │ Router │ │
│ │ (导航) │ │
│ └─────────┘ │
└───────────────┘
如果把 MovieListViewModel 改名为 MovieListInteractor,就是 100% 的 VIPER,但同时又没有违背 MVVM 的原则。
具体实现
Router 实现(支持 A/B 测试切换):
// 旧版 UI Router
final class MovieListRouterImpl: MovieListRouter {
private weak var sourceViewController: UIViewController?
func showDetailView(for movie: Movie) {
let detailVC = MovieDetailViewController(movie: movie)
let nav = UINavigationController(rootViewController: detailVC)
sourceViewController?.present(nav, animated: true)
}
}
// 新版 UI Router(A/B 测试用)
final class MovieListRouterV2: MovieListRouter {
private weak var sourceViewController: UIViewController?
func showDetailView(for movie: Movie) {
let detailVC = MovieDetailViewControllerV2(movie: movie)
let nav = UINavigationController(rootViewController: detailVC)
sourceViewController?.present(nav, animated: true)
}
}
VIPER Presenter 实现:
final class VIPERMovieListPresenterImpl: VIPERMovieListPresenter {
private let viewModel: MovieListViewModel
private let router: MovieListRouter
func didTapOnMovie(at index: Int) {
let movie = movies[index]
router.showDetailView(for: movie) // 导航交给 Router
}
func viewModelDidUpdate(_ model: MovieListViewModel) {
movies = model.movies
let presentations = movies.map { presentation(from: $0) }
_onUpdate?(presentations)
}
}
View 中注入 Router 实现 A/B 测试:
// 使用旧版 UI
let router = MovieListRouterImpl(sourceViewController: nav)
let viperVC = MovieListViewController_VIPER(router: router)
// 切换为 V2 新版 UI,只需换 Router,其他代码零改动
let routerV2 = MovieListRouterV2(sourceViewController: nav)
let viperVC = MovieListViewController_VIPER(router: routerV2)
八、三阶段对比总结
组件演化
| 阶段 | View | Presenter | ViewModel/Interactor | Router | 核心变化 |
|---|---|---|---|---|---|
| MVVM | 持有 ViewModel,处理展示和导航 | — | 业务逻辑 + 数据获取 | — | 最简形式 |
| +Presenter | 只消费 Presentation,导航仍在 View | 数据→展示格式转化 | 业务逻辑 + 数据获取 | — | 抽离展示逻辑,跨平台复用 |
| VIPER | 只消费 Presentation,不关心导航 | 协调 ViewModel 和 Router | 业务逻辑 + 数据获取 | 导航逻辑 | 抽离导航逻辑,支持 A/B 测试 |
选择决策树
需要多平台? ──否──► MVVM 就够了
│
是
│
▼
引入 Presenter(复用展示逻辑)
│
需要 A/B 测试或复杂导航? ──否──► 停在 Presenter 阶段
│
是
│
▼
引入 Router → 完整 VIPER
九、核心思想
架构是演化出来的,不是设计出来的。VIPER 是 MVC → MVVM → VIPER 这条扩展性演化路径上最细化的版本。
建议的实践方式:
- 从简化版开始:只有 View、Interactor 和 Entities,和 MVVM 差不多
- 需要快速改 UI 时:加 Presenter
- 有复杂可复用路由逻辑时:加 Router
- 实现需求前:先设计协议接口
不要为了架构而架构。如果你的 App 只在一个平台运行、没有 A/B 测试需求、导航逻辑简单,MVVM 完全够用。
十、Demo 使用方式
三个 Tab 分别展示三个阶段,每个阶段都是独立可运行的电影列表应用。阶段三的 Router 可在 ContentView.swift 中切换 MovieListRouterImpl / MovieListRouterV2 体验 A/B 测试效果。
本文基于「VIPER 和 MVVM 到底有什么区别」整理,并补充了完整的可运行代码 Demo。