从 MVVM 到 VIPER:架构是演化出来的,不是设计出来的

一、前言

如果你开发过移动端 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 ──► │  (业务逻辑)    │
└──────────┘               └───────────────┘

数据流

  1. View 把自己作为 ViewModel 的 delegate
  2. 用户点击重载
  3. View 调用 viewModel.fetchMovies()
  4. 数据获取成功后,ViewModel 通知 delegate(view)
  5. 调用 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 ──► │  (业务逻辑)    │
└──────────┘             └───────────────┘             └───────────────┘

数据流

  1. View 把自己作为 Presenter 的 delegate
  2. Presenter 把自己作为 ViewModel 的 delegate
  3. 用户点击重载
  4. View 调用 presenter.reload()
  5. Presenter 调用 viewModel.fetchMovies()
  6. 数据获取成功后,ViewModel 通知 delegate(presenter)
  7. Presenter 将 Movie 转化为 MoviePresentation,通知 delegate(view)
  8. 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 这条扩展性演化路径上最细化的版本。

建议的实践方式:

  1. 从简化版开始:只有 View、Interactor 和 Entities,和 MVVM 差不多
  2. 需要快速改 UI 时:加 Presenter
  3. 有复杂可复用路由逻辑时:加 Router
  4. 实现需求前:先设计协议接口

不要为了架构而架构。如果你的 App 只在一个平台运行、没有 A/B 测试需求、导航逻辑简单,MVVM 完全够用。


十、Demo 使用方式

三个 Tab 分别展示三个阶段,每个阶段都是独立可运行的电影列表应用。阶段三的 Router 可在 ContentView.swift 中切换 MovieListRouterImpl / MovieListRouterV2 体验 A/B 测试效果。

本文基于「VIPER 和 MVVM 到底有什么区别」整理,并补充了完整的可运行代码 Demo。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 小学在做数学题的时候,就学过真命题和假命题,那时候只道这是考试要考的内容,没有想到这是逻辑学的基础。一直到高中,我...
    莫琳芽儿阅读 80评论 0 0
  • 投射我儿读书明理,修身做人,每天情绪平和稳定,阳光快乐,越来越会调节自己的情绪和压力。 投射我儿成为更好的自己,多...
    花开生两面阅读 77评论 0 0
  • 你好,我是小熙,欢迎打开我的日复盘 每天给自己20分钟复盘时间,重新察审视自己一下吧。总是忙着低头砍树,记得抽空磨...
    和小熙一起变好阅读 70评论 0 0
  • 3日 上午整理2日照片,不多。午间抄写《金刚经》第三分。 13:00出门,至大葆台西汉墓遗址(北京考古遗址博物馆)...
    可爸亮阅读 59评论 0 0
  • 今天是10月4日,农历八月十三,星期六。每个人都向往诗和远方,但平凡的生活才是日常,中年人的优雅,不是避开车马喧嚣...
    快乐大拙阅读 70评论 0 3

友情链接更多精彩内容