A dumb UI is a good UI: Using MVP in iOS with swift

前几天面试了一家公司, 问了我一些关于 iOS 开发中的概念性问题, 比如通知和代理的区别, MVVM, MVP, MVC 这些设计模式分别是如何实现的, 虽然平常开发中经常听到这些概念, 但是完全没有刻意去记下它们之间的区别和实现.
主要是我个人认为任何设计模式都是和不能独立于业务逻辑而存在的, 适合什么用什么并不需要刻意去记下这些东西, 需要用到的时候查下资料就好了.
那么话说回来为什么要写这篇博文呢?
自问自答一下吧: 我想证明一个道理, 只要你不是一个咸鱼程序员任何概念性的问题, 只需要一小时就可以搞明白

MVC 模式介绍

当涉及到ios应用程序的开发时, 模型视图控制器是一种常见的设计模式.
通常视图层由 UIKit 中的元素组成, 这些元素通过程序或 xib 文件定义, 模型层包含应用程序的业务逻辑, 控制器层(由 UIViewController 类表示)是模型和视图之间的粘合剂.

bildschirmfoto-2016-02-01-um-22 23 46

这种模式的一个很好的部分是将业务逻辑和业务规则封装在模型层中. 但是, UIViewController 仍然包含与 UI 有关的逻辑, 这意味着如下:

  • 调用业务逻辑并将结果绑定到视图
  • 管理视图元素
  • 将来自模型层的数据转换为友好的格式
  • 导航逻辑
  • 管理 UI 状态
  • 更多…

承担所有这些工作, UIViewController 将会变得巨大而难以维护和测试.

所以, 现在是时候考虑改进 MVC 来处理这些问题了.
我们称之为改进 模型(Model)-视图(View)-主持人(Presenter) MVP.

MVP 模式介绍

MVP 模式在1996年由 Mike Potel 首次引入, 并且多年来进行了多次讨论.
在他的文章中, GUI架构 Martin Fowler 讨论了这种模式, 并将其与其他管理 UI 代码的模式进行了比较.

有很多 MVP 的变体, 它们之间有很小的差异.
在这篇文章中, 我选择了目前应用程序开发中常用的常用一种.
这个变体的特征是:

  • MVP的视图部分包括uiview和UIViewController.
  • 视图(View)将用户交互委托给主持人(Presenter).
  • 主持人包含处理用户交互的逻辑.
  • 主持人(Presenter)与模型(Model)层进行通信, 将数据转换为UI友好格式, 并更新视图(View).
  • 主持人对 UIKit 没有依赖关系.
  • 视图是被动的 (dump)

bildschirmfoto-2016-02-01-um-22 23 57

MVP 操作示例

以下示例将向您展示如何在操作中使用 MVP

我们的示例是一个非常简单的应用程序, 它只显示一个简单的用户列表. 您可以从这里获得完整的源代码: https://github.com/iyadagha/iOS-mvp-sample .

Model

让我们从用户的简单数据模型开始:

1
2
3
4
5
6
struct User {
let firstName: String
let lastName: String
let email: String
let age: Int
}

UserService

那么我们实现一个简单的用户服务, 即异步返回用户列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserService {
//the service delivers mocked data with a delay
func getUsers(callBack:([User]) -> Void){
let users = [User(firstName: "Iyad", lastName: "Agha", email: "iyad@test.com", age: 36),
User(firstName: "Mila", lastName: "Haward", email: "mila@test.com", age: 24),
User(firstName: "Mark", lastName: "Astun", email: "mark@test.com", age: 39)
]
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(2 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
callBack(users)
}
}
}

UserPresenter

下一步是编写userpresenter.
首先我们需要用户的数据模型, 可以直接在视图中使用.
它包含根据需要从视图中正确格式化的数据:

1
2
3
4
struct UserViewData{
let name: String
let age: String
}

UserView

之后, 我们需要对视图进行抽象, 这可以在 Presenter 不知道 UIViewController 的情况下使用.
我们通过定义一个协议 UserView 来做到这一点:

1
2
3
4
5
6
protocol UserView: NSObjectProtocol {
func startLoading()
func finishLoading()
func setUsers(users: [UserViewData])
func setEmptyUsers()
}

该协议将在 Presenter 中使用, 稍后将在 UIViewController 中实现. 基本上, 协议包含了在 Presenter 中控制 View 的函数调用.

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
31
32
class UserPresenter {
private let userService:UserService
weak private var userView : UserView?
init(userService:UserService){
self.userService = userService
}
func attachView(view:UserView){
userView = view
}
func detachView() {
userView = nil
}
func getUsers(){
self.userView?.startLoading()
userService.getUsers{ [weak self] users in
self?.userView?.finishLoading()
if(users.count == 0){
self?.userView?.setEmptyUsers()
}else{
let mappedUsers = users.map{
return UserViewData(name: "\($0.firstName) \($0.lastName)", age: "\($0.age) years")
}
self?.userView?.setUsers(mappedUsers)
}
}
}
}

我们将在后面看到 Presenter 可以通过函数attachView(view:UserView)attachView(view:UserView)来更好地控制 UIViewContoller 的生命周期方法
请注意, 将User转换为UserViewData是 Presenter 的责任.
还要注意, userView必须weak以避免保留周期.

UserViewController

实现的最后一部分是UserViewController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserViewController: UIViewController {
@IBOutlet weak var emptyView: UIView?
@IBOutlet weak var tableView: UITableView?
@IBOutlet weak var activityIndicator: UIActivityIndicatorView?
private let userPresenter = UserPresenter(userService: UserService())
private var usersToDisplay = [UserViewData]()
override func viewDidLoad() {
super.viewDidLoad()
tableView?.dataSource = self
activityIndicator?.hidesWhenStopped = true
userPresenter.attachView(self)
userPresenter.getUsers()
}
}

我们的 ViewController 有一个 tableView 来显示用户列表、一个 emptyView (如果没有用户时显示)和一个当应用程序正在加载用户时显示的 activityIndicator. 此外, 它还有一个 userPresenter 和一个用户列表.

viewDidLoad方法中, UserViewController将自己连接到 Presenter.
这是可行的, 因为我们很快就会看到 UserViewController 实现了 UserView 协议.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension UserViewController: UserView {
func startLoading() {
activityIndicator?.startAnimating()
}
func finishLoading() {
activityIndicator?.stopAnimating()
}
func setUsers(users: [UserViewData]) {
usersToDisplay = users
tableView?.hidden = false
emptyView?.hidden = true;
tableView?.reloadData()
}
func setEmptyUsers() {
tableView?.hidden = true
emptyView?.hidden = false;
}
}

正如我们所看到的, 这些函数不包含复杂的逻辑, 他们只是在进行纯视图管理.

UITableViewDataSource

最后, UITableViewDataSource 实现非常基本, 看起来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension UserViewController: UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return usersToDisplay.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "UserCell")
let userViewData = usersToDisplay[indexPath.row]
cell.textLabel?.text = userViewData.name
cell.detailTextLabel?.text = userViewData.age
cell.textLabel
return cell
}
}

mvp-ios-e1454670703144

单元测试

做MVP的好处之一是能够在不测试UIViewController本身的情况下测试大部分UI逻辑.
如果我们对我们的 Presenter 有一个很好的单元测试覆盖范围, 我们就不需要为UIViewController编写单元测试了.

现在让我们看看如何测试我们的UserPresenter. 首先, 我们定义两个mock对象. 其中一个mock是UserService, 以使它提供所需的用户列表. 另一个mock是UserView, 以验证这些方法是否被正确调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UserServiceMock: UserService {
private let users: [User]
init(users: [User]) {
self.users = users
}
override func getUsers(callBack: ([User]) -> Void) {
callBack(users)
}
}
class UserViewMock : NSObject, UserView{
var setUsersCalled = false
var setEmptyUsersCalled = false
func setUsers(users: [UserViewData]) {
setUsersCalled = true
}
func setEmptyUsers() {
setEmptyUsersCalled = true
}
}

现在, 我们可以测试当服务提供一个非空用户列表时, Presenter 的行为是否正确.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserPresenterTest: XCTestCase {
let emptyUsersServiceMock = UserServiceMock(users:[User]())
let towUsersServiceMock = UserServiceMock(users:[User(firstName: "firstname1", lastName: "lastname1", email: "first@test.com", age: 30),
User(firstName: "firstname2", lastName: "lastname2", email: "second@test.com", age: 24)])
func testShouldSetUsers() {
//given
let userViewMock = UserViewMock()
let userPresenterUnderTest = UserPresenter(userService: towUsersServiceMock)
userPresenterUnderTest.attachView(userViewMock)
//when
userPresenterUnderTest.getUsers()
//verify
XCTAssertTrue(userViewMock.setUsersCalled)
}
}

同样的, 如果服务返回一个空的用户列表, 我们也可以测试 Presenter 是否正确工作.

1
2
3
4
5
6
7
8
9
10
11
12
func testShouldSetEmptyIfNoUserAvailable() {
//given
let userViewMock = UserViewMock()
let userPresenterUnderTest = UserPresenter(userService: emptyUsersServiceMock)
userPresenterUnderTest.attachView(userViewMock)
//when
userPresenterUnderTest.getUsers()
//verify
XCTAssertTrue(userViewMock.setEmptyUsersCalled)
}

Where to go from there

我们已经看到了MVP是MVC的演进. 我们只需要将UI逻辑放在一个名为 Presenter 的额外组件中, 并 被动的 (dump) 使我们的UIViewController.

MVP的特点之一是 Presenter 和 View 互相认识.
在这种情况下, 视图 UIViewController 具有对演示者的引用, 反之亦然.
尽管可以使用反应式编程来删除演示者中使用的视图的参考.
使用 ReactiveCocoaRxSwift 等响应式框架, 可以构建一个体系结构, 其中只有 View 知道 Presenter, 反之亦然.
在这种情况下, 架构将被称为 MVVM.

如果你想在iOS中了解更多关于MVVM的信息, 请查看以下帖子:
MVVM Tutorial with ReactiveCocoa
Implementing MVVM in iOS with RxSwift

转载

http://iyadagha.com/using-mvp-ios-swift/