iOS应用架构设计:MVC、MVVM、VIPER,我在杭州卷完代码还要卷公考

黄勇·
2025-12-14 12:10
阅读 539

上周五晚上十点半,我合上 MacBook Pro,盯着屏幕上那堆耦合到天际的 ViewController 代码,脑子里只有一个念头:这玩意要是能重构,我现在就去西湖边跑十圈

我是谁?一个在杭州某二线互联网公司搬砖的 iOS 程序员,白天写 Swift 搞性能优化,晚上刷行测申论准备考公。没错,就是那种“白天被产品经理催需求,晚上被粉笔APP催打卡”的夹心饼干。阿里网易就在隔壁,机会多得像西湖边的共享单车,但我也清楚——35岁前不上岸,可能就得靠骑单车送外卖了。

说回正题。我们项目是个电商类 App,去年双11期间线上崩了两次,原因?ViewController 超过 2000 行,网络请求、UI 更新、业务逻辑全塞一块儿,改个按钮颜色都能触发支付回调。运维老哥半夜打电话骂我:“你这代码是拿脚写的吧?”我当时真想砸电脑。

痛定思痛,领导拍板:架构必须重构。可选方案有仨:MVC、MVVM、VIPER。别看都是字母缩写,背后可是完全不同的哲学。今天这篇不是教科书式的理论对比,而是我踩坑、加班、被测试追着问进度后的真实复盘。


起点:MVC —— 苹果给的糖,吃多了也齁

刚入行时,Apple 官方文档和《iOS Programming: The Big Nerd Ranch Guide》(这本书我翻烂了)都把 MVC 当成默认答案。Model-View-Controller,听起来很美:Model 管数据,View 管展示,Controller 协调一切。

但现实是,iOS 的 MVC 很容易变成 Massive View Controller

// 典型的“Massive VC”现场
class ProductDetailViewController: UIViewController {
    @IBOutlet weak var priceLabel: UILabel!
    @IBOutlet weak var buyButton: UIButton!

    private var product: Product?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchProduct() // 网络请求
        setupUI()      // UI配置
        trackEvent()   // 埋点
        checkInventory() // 库存逻辑
    }

    func fetchProduct() {
        NetworkManager.shared.request(.product(id: "123")) { [weak self] result in
            switch result {
            case .success(let data):
                self?.product = Product(from: data)
                self?.updateUI() // 直接操作UI
            case .failure(let error):
                self?.showError(error)
            }
        }
    }
}

这段代码是不是眼熟?几乎每个 iOS 项目初期都会这么写。问题在哪?Controller 既要管网络,又要管 UI 更新,还得处理业务规则。测试?别想了,Mock 都不知道从哪下手。

我们在上一个版本就这么干的,结果双11当天用户点击“立即购买”没反应——因为库存检查逻辑和网络回调混在一起,异步顺序出错。测试小妹幽幽地说:“你们程序员是不是觉得用户不会同时点两次按钮?”


进阶:MVVM —— 数据绑定真香,但别信 SwiftUI 的鬼话

被教训后,我翻开了《iOS Architecture Patterns》这本书(技术群里人手一本),决定试试 MVVM(Model-View-ViewModel)。

核心思想很简单:把 Controller 的业务逻辑抽到 ViewModel 里,View 只负责展示,ViewModel 负责提供数据和状态。配合 RxSwift 或 Combine,还能玩响应式编程。

// ViewModel 把脏活累活扛了
class ProductDetailViewModel {
    let product = CurrentValueSubject<Product?, Never>(nil)
    let isLoading = CurrentValueSubject<Bool, Never>(false)
    let errorMessage = CurrentValueSubject<String?, Never>(nil)

    func loadProduct(id: String) {
        isLoading.send(true)
        NetworkManager.shared.request(.product(id: id)) { [weak self] result in
            DispatchQueue.main.async {
                self?.isLoading.send(false)
                switch result {
                case .success(let data):
                    self?.product.send(Product(from: data))
                case .failure(let error):
                    self?.errorMessage.send(error.localizedDescription)
                }
            }
        }
    }
}

// ViewController 瘦成一道闪电
class ProductDetailViewController: UIViewController {
    @IBOutlet weak var priceLabel: UILabel!
    
    private let viewModel = ProductDetailViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.loadProduct(id: "123")
    }

    private func bindViewModel() {
        viewModel.product
            .compactMap { $0 }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] product in
                self?.priceLabel.text = "¥\(product.price)"
            }
            .store(in: &cancellables)
    }
}

效果立竿见影:ViewController 行数从 1800+ 降到 300 以内,ViewModel 可以独立单元测试,连测试小妹都夸我“这次没埋雷”。

但别高兴太早。MVVM 的坑在于“过度设计”。有些团队一上来就搞 RxSwift + RxCocoa,结果新人看了代码直呼“这是 Swift 还是 Rx 咒语?”而且,SwiftUI 虽然原生支持 MVVM,但实际项目里混合使用 UIKit 和 SwiftUI 时,状态管理容易撕裂

有一次我用 @PublishedCurrentValueSubject 混着用,结果状态更新不一致,UI 卡在 loading 状态。查了三天,最后发现是 Combine 的调度队列没切主线程。那一刻,我真的想转行卖煎饼。


终极解?VIPER —— 架构洁癖患者的狂欢

听说阿里某些核心 App 用 VIPER,我心动了。VIPER 是 View-Interactor-Presenter-Entity-Router 的缩写,把职责拆到极致,每个模块只干一件事

  • View: 只管 UI 展示和用户交互
  • Presenter: 处理 View 的输入,协调 Interactor
  • Interactor: 包含业务逻辑
  • Entity: 纯数据模型
  • Router: 负责页面跳转

听起来是不是特别“干净”?但代价是:文件数量爆炸。一个简单页面要建 5 个类。

// Interactor - 专注业务
class ProductDetailInteractor {
    weak var output: ProductDetailInteractorOutput?

    func fetchProduct(id: String) {
        NetworkManager.shared.request(.product(id: id)) { result in
            switch result {
            case .success(let data):
                let product = Product(from: data)
                self.output?.didFetchProduct(product)
            case .failure(let error):
                self.output?.didFailWithError(error)
            }
        }
    }
}

// Presenter - 中间人
class ProductDetailPresenter {
    weak var view: ProductDetailViewInput?
    var interactor: ProductDetailInteractorInput
    var router: ProductDetailRouterInput

    func viewDidLoad() {
        view?.showLoading()
        interactor.fetchProduct(id: "123")
    }

    func didTapBuyButton() {
        router.routeToPayment()
    }
}

我们拿一个小功能模块试水 VIPER。结果?开发效率暴跌。每天光在五个文件之间跳转就能把我累死。更糟的是,App Store 审核时因为 Router 跳转逻辑复杂,被误判为“隐藏功能”,差点被拒。

但 VIPER 也不是一无是处。对于高度复杂的业务(比如金融交易、订单流程),它的清晰边界确实能降低长期维护成本。只是对我们这种电商 App,有点杀鸡用牛刀。


三架构横向对比:别被名词忽悠了

维度 MVC MVVM VIPER
上手难度 ⭐⭐ ⭐⭐⭐⭐
代码量 少(但臃肿) 中等 极多
可测试性 极好
团队协作 容易冲突 较清晰 需严格规范
适合场景 小型/原型项目 中大型项目 超大型/高稳定性要求

我的结论很务实:没有银弹,只有权衡

  • 新项目起步?MVC 快速验证 MVP。
  • 项目成长到中等规模?MVVM 是甜区,尤其配合 Combine(毕竟 Apple 自家亲儿子)。
  • 核心交易链路?可以局部用 VIPER,但别全盘照搬。

考公程序员的终极心得:架构是手段,不是目的

现在我们的 App 用的是 MVVM + 轻量级 Coordinator 模式(Router 的简化版),既保证了可维护性,又不至于让新人第一天就提离职。

上周上线新版本,零崩溃。测试小妹居然请我喝了杯瑞幸——虽然她说“下次别再把埋点逻辑写在 ViewModel 里了”。

回头想想,学架构不是为了炫技,而是为了睡个安稳觉。毕竟,我晚上还得刷 50 道言语理解题呢。考公路上,代码整洁一点,bug 少一点,就能多留半小时给申论素材积累。

最后送大家一句我在《iOS组件化架构》书里看到的话:“好的架构,是让后来者能轻松接手,而不是让面试官眼前一亮。”

共勉。毕竟在杭州,我们不仅要卷代码,还得卷上岸。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝