笨拙的UI是一个好的UI:在iOS中使用Swift的MVP

由Mohamed Iyad Tamer Agha撰写

在开发iOS应用程序时,Model-View-Controller是一种常见的设计模式。 通常,视图层由以编程方式定义或在xib文件中定义的UIKit元素组成,模型层包含应用程序的业务逻辑,而由UIViewController类表示的控制器层则是模型和视图之间的粘合剂。

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

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

承担所有这些责任,ViewController经常变得庞大,并且难以维护和测试。

因此,现在该考虑改进MVC来解决这些问题了。 我们将此改进称为模型视图演示者MVP。

MVP模式是由Mike Potel于1996年首次提出的,多年来讨论了多次。 Martin Fowler在他的GUI体系结构文章中讨论了这种模式,并将其与其他用于管理UI代码的模式进行了比较。
MVP有很多变体,但它们之间的差异很小。 在这篇文章中,我选择了似乎在当今的应用程序开发中最常用的通用示例。 此变体的特征是:

  • MVP的视图部分同时包含UIViews和UIViewController
  • 该视图将用户交互委托给演示者
  • 演示者包含处理用户交互的逻辑
  • 演示者与模型层进行通信,将数据转换为UI友好格式,并更新视图
  • 演示者不依赖于UIKit
  • 视图是被动的(转储)

以下示例将向您展示如何实际使用MVP。

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

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

[js]
struct用户{
让firstName:字符串
让lastName:字符串
让电子邮件:字符串
年龄:整数
}
[/ js]

然后,我们实现一个简单的UserService,它异步返回用户列表:

[js]
类UserService {

//服务延迟交付模拟数据
func getUsers(callBack:([[User])-> Void){
let users = [User(firstName:“ Iyad”,lastName:“ Agha”,电子邮件:“ iyad@test.com”,年龄:36),
用户(名字:“ Mila”,姓氏:“ Haward”,电子邮件:“ mila@test.com”,年龄:24),
用户(名字:“ Mark”,姓氏:“ Astun”,电子邮件:“ mark@test.com”,年龄:39)
]

让delayTime = dispatch_time(DISPATCH_TIME_NOW,Int64(2 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime,dispatch_get_main_queue()){
callBack(用户)
}
}
}
[/ js]

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

[js]
struct UserViewData {
命名:字符串
让年龄:字符串
}
[/ js]

之后,我们需要一个视图的抽象,可以在演示者中使用它,而无需了解UIViewController。 我们通过定义协议UserView来做到这一点:

[js]
协议UserView:NSObjectProtocol {
func startLoading()
func finishLoading()
func setUsers(用户:[UserViewData])
func setEmptyUsers()
}
[/ js]

此协议将在演示者中使用,稍后将通过UIViewController实现。 基本上,该协议包含在演示者中调用的用于控制视图的功能。

演示者本身看起来像:

[js]
类UserPresenter {
私人让userService:UserService
弱私有var userView:UserView?

init(userService:UserService){
self.userService = userService
}

func attachView(view:UserView){
userView =视图
}

func detachView(){
userView =无
}

func getUsers(){
self.userView?.startLoading()
userService.getUsers {[弱自我]用户
自我?.userView?.finishLoading()
if(users.count == 0){
self?.userView?.setEmptyUsers()
}其他{
让mapedUsers = users.map {
return UserViewData(name:“ \($ 0.firstName)\($ 0.lastName)”,年龄:“ \($ 0.age)年”)
}
自我?.userView?.setUsers(mappedUsers)
}

}
}
}

[/ js]

演示者使用函数attachView(view:UserView)attachView(view:UserView)来对UIViewContoller的生命周期方法进行更多控制,我们将在后面看到。
请注意,将User转换为UserViewData是演示者的责任。 还要注意, userView必须很弱以避免保留周期。

实现的最后一部分是UserViewController:

[js]
类UserViewController:UIViewController {

@IBOutlet弱var emptyView:UIView?
@IBOutlet弱var tableView:UITableView?
@IBOutlet弱var activityIndi​​cator:UIActivityIndi​​catorView?

私人让userPresenter = UserPresenter(userService:UserService())
私人var usersToDisplay = [UserViewData]()

覆盖func viewDidLoad(){
super.viewDidLoad()
tableView?.dataSource =自我
activityIndi​​cator?.hidesWhenStopped = true

userPresenter.attachView()
userPresenter.getUsers()
}

}
[/ js]

我们的ViewController有一个tableView用于显示用户列表,如果没有可用用户,则显示一个emptyView,以及在应用加载用户时显示的activityIndi​​cator。 此外,它还有一个userPresenter和一个用户列表。

viewDidLoad方法中,UserViewController将自身附加到演示者。 之所以可行,是因为我们将很快看到UserViewController实现了UserView协议。

[js]
扩展UserViewController:UserView {

func startLoading(){
activityIndi​​cator?.startAnimating()
}

func finishLoading(){
activityIndi​​cator?.stopAnimating()
}

func setUsers(用户:[UserViewData]){
usersToDisplay =用户
tableView?.hidden = false
emptyView?.hidden = true;
tableView?.reloadData()
}

func setEmptyUsers(){
tableView?.hidden = true
emptyView?.hidden = false;
}
}
[/ js]

如我们所见,这些函数不包含复杂的逻辑,它们只是在进行纯视图管理。

最后, UITableViewDataSource实现非常基础,如下所示:

[js]
扩展UserViewController:UITableViewDataSource {
func tableView(tableView:UITableView,numberOfRowsInSection部分:Int)-> Int {
返回usersToDisplay.count
}

func tableView(tableView:UITableView,cellForRowAtIndexPath indexPath:NSIndexPath)-> UITableViewCell {
let cell = UITableViewCell(样式:UITableViewCellStyle.Subtitle,reuseIdentifier:“ UserCell”)
让userViewData = usersToDisplay [indexPath.row]
cell.textLabel?.text = userViewData.name
cell.detailTextLabel?.text = userViewData.age
cell.textLabel
返回单元
}
}
[/ js]

单元测试

进行MVP的好处之一是能够测试UI逻辑的最大部分而无需测试UIViewController本身。 因此,如果我们对演示者有很好的单元测试覆盖率,则不再需要为UIViewController编写单元测试。

现在让我们看看如何测试UserPresenter。 首先,我们定义要使用的拖车模拟。 一种模拟是UserService,它可以提供所需的用户列表。 另一个模拟是UserView的,以验证方法是否被正确调用。

[js]
类UserServiceMock:UserService {
私人让用户:[用户]
init(用户:[用户]){
self.users =用户
}
覆盖func getUsers(callBack:([User])-> Void){
callBack(用户)
}

}

类UserViewMock:NSObject,UserView {
var setUsersCalled = false
var setEmptyUsersCalled = false

func setUsers(用户:[UserViewData]){
setUsersCalled = true
}

func setEmptyUsers(){
setEmptyUsersCalled = true
}
}
[/ js]

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

[js]
类UserPresenterTest:XCTestCase {

让emptyUsersServiceMock = UserServiceMock(users:[User]())

让towUsersServiceMock = UserServiceMock(用户:[用户(firstName:“ firstname1”,lastName:“ lastname1”,电子邮件:“ first@test.com”,年龄:30)”,
用户(firstName:“ firstname2”,lastName:“ lastname2”,电子邮件:“ second@test.com”,年龄:24)])

func testShouldSetUsers(){
//给定
让userViewMock = UserViewMock()
让userPresenterUnderTest = UserPresenter(userService:towUsersServiceMock)
userPresenterUnderTest.attachView(userViewMock)

//什么时候
userPresenterUnderTest.getUsers()

//校验
XCTAssertTrue(userViewMock.setUsersCalled)
}
}

[/ js]

以相同的方式,我们可以测试如果服务返回一个空用户列表,演示者是否可以正常工作。

[js]
func testShouldSetEmptyIfNoUserAvailable(){
//给定
让userViewMock = UserViewMock()
让userPresenterUnderTest = UserPresenter(userService:emptyUsersServiceMock)
userPresenterUnderTest.attachView(userViewMock)

//什么时候
userPresenterUnderTest.getUsers()

//校验
XCTAssertTrue(userViewMock.setEmptyUsersCalled)
}

[/ js]

从那里去哪里

我们已经看到,MVP是MVC的演变。 我们只需要将UI逻辑放在一个名为Presenter的额外组件中,并使我们的UIViewController钝化(转储)。

MVP的特征之一是演示者和视图者彼此了解。 视图(在这种情况下为UIViewController)具有演示者的引用,反之亦然。
尽管可以使用反应式编程删除演示者中使用的视图的引用。 使用诸如ReactiveCocoa或RxSwift之类的反应性框架,可以构建一种架构,其中仅视图知道演示者,反之亦然。 在这种情况下,该架构将被称为MVVM。

如果您想了解有关iOS中MVVM的更多信息,请查看以下文章:
带有ReactiveCocoa的MVVM教程
使用RxSwift在iOS中实现MVVM