Home MVC, MVP, MVVM, VIPER and MVVMC(SwiftUI) All In One
Post
Cancel

MVC, MVP, MVVM, VIPER and MVVMC(SwiftUI) All In One

Architecture

iOS开发最初遵循原始的MVC架构, 但是随着业务的演进代码越来越复杂, 很容易变成Massive View Controller, 最终导致代码难以维护, 难以测试, 难以复用. 为了解决这个问题逐步演化出:

  • MVP
  • MVVM
  • VIPER
  • MVVM-C

等架构. 今天我们就用各个架构实现一个ToDoList来看看他们是如何解决问题的以及各自的优缺点.

要实现的功能是一个to do list页面, 功能如下:

  • 点击删除条目
  • 点击+添加条目
  • 输入字符超过3个(包含)才可添加

示例如下:

1. MVC

源代码

标准的MVC结构如下, 我们把model和business logic都放在controller中.

  • 接收用户interaction
  • 更新model
  • 更新view

1
2
3
4
5
6
7
8
9
10
11
// 点击删除item
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard indexPath.section == Section.todos.rawValue else {
            return
        }
        
        todos.remove(at: indexPath.row)    //更新model
        title = "TODO - (\(todos.count))"  //更新model
        tableView.reloadData() //刷新View
    }

问题

  • 无法测试business logic是否正确, 包括:
    1
    2
    3
    4
    
    1. 添加时button的enabled状态是否正确
    2. 添加时新增的条目是否正确
    3. 是否删除正确的条目
    ...
    
  • View与business logic耦合在一起, 无法复用
    1
    
    1. 如果要换个界面比如卡片式, 整个代码都要修改, 无法复用现有的business logic
    

优点

  • 代码量少, 开发速度快
  • 不需要太多经验就能开发维护

MVP

源代码

MVP架构中, 将View+Controller组成一个PassiveView, PassiveView负责转发用户交互事件, 业务逻辑都在Presenter中实现, 由Presenter负责更新视图.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protocol ToDoListView: AnyObject {
    func update(list: [String], title: String)
    func enableAdd(_ enable: Bool)
}

protocol ToDoListPresenter {
    func load()
    func remove(at index: Int)
    func edit(_ text: String)
    func add()
}

class ToDoListPresenterImpl: ToDoListPresenter {
    unowned var view: ToDoListView

    var todos: [String] = [] {
        didSet {
            view.update(list: todos, title: "TODO - (\(self.todos.count))")
        }
    }
    var text = "" {
        didSet {
            view.enableAdd(text.count >= 3)
        }
    }

    init(_ view: ToDoListView) {
        self.view = view
    }
}

Controller和Presenter的绑定: ```swift … let controller: TableViewController = UIStoryboard(name: “Table”, bundle: nil) .instantiateViewController(withIdentifier: “tableViewController”) as! TableViewController

let presenter = ToDoListPresenterImpl(controller) controller.presenter = presenter

controller.navigationItem.hidesBackButton = true self.navigationController?.pushViewController(controller, animated: false) …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
><span style="color:red">注意`view`持有`presenter`, 而`presenter`连接`unowned view`</span>

业务逻辑在`presenter`中, 很容易通过mock view进行测试(代码覆盖率达到`97.8`).
![](mvp-test.png)

><span style="color:red">问题</span>
- 需要手动将数据的变化绑定到view
> 优点
- 业务逻辑与view隔离, 容易测试
- 业务逻辑与view隔离, 容易复用, 如果只修改界面, 换一个`ToDoListView`实现即可.

## MVVM
[源代码](https://github.com/zteshadow/best-practice/tree/main/native-ios/MVVM)

![](mvvm.png)

从架构图中也可以看到, `MVVM`架构与`MVP`架构基本相同, 但有以下几点区别
- 对视图的更新, 在`MVP`中是手动实现的, 而`MVVM`中是自动完成的(用callback, combine, RxSwift技术等)
- `view model`的设计原则是: 持有`view`中对应的状态, 修改状态 -> 自动更新view

这点稍微比较隐晦, 比如在Presenter中, 更新todos会同步更新title, 因此Presenter中可以不持有title, 但是设计view model的时候, 一定是将view中需要的状态都保存在view model中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
```swift
class ToDoListViewModel {
    @Published var todos: [String] = [] {
        didSet {
            title = "TODO - (\(self.todos.count))"
        }
    }
    @Published var title: String = ""
    @Published var enableAdd: Bool = false

    var text = "" {
        didSet {
            if text.count >= 3 {
                enableAdd = true
            } else {
                enableAdd = false
            }
        }
    }
}
  • businessview的耦合不同, 如果使用combine来实现MVVM中的绑定, 那么这个view model是和view紧密结合在一起的, 不像MVPviewPresenter解耦的那么彻底, 更适合复用.

  • 测试方式也不同, 同上, 如果使用combine来实现MVVM中的绑定, 那么一些基本的数据操作逻辑是不用测试的.

VIPER

源代码

与MVP的区别

  • Presenter进一步的细化, 分拆出来InteractorRouter, 分别负责外部数据交互以及路由

MVVMC

architecture

Typical coordinator(统筹者,协调人) ```swift public protocol MyTicketsCoordinator: Coordinator { /// Creates a new view to show for the home-page tab. func createMyTicketsView() -> AnyView /// or func pushMyTicketsScreen() -> ChildCoordinator }

/// The MyTickets public APIs to be used by other modules public protocol MyTicketsAPI { /// Creates a new coordinator for the home page. func createMyTicketsCoordinator(router: RoutingAPI) -> MyTicketsCoordinator }

```

总结

架构的最终目的

  • UI可复用可更新
  • 业务逻辑可测试
This post is licensed under CC BY 4.0 by the author.