原文:《A dumb UI is a good UI: Using MVP in iOS with swift》 链接:http://iyadagha.com/using-mvp-ios-swift/
在开发iOS应用程序时,Model-View-Controller是一种常见的设计模式。 通常,View层由UIKit中的元素组成,这些元素由代码定义或xib文件定义,Model层包含应用程序的业务逻辑,并且由UIViewController类表示的Controller层是Model和View之间的粘合剂。
这种模式的一个很好的部分是将业务逻辑和业务规则封装在Model层中。但是,UIViewController仍然包含UI相关的逻辑,它的意思是:
- 调用业务逻辑并将结果绑定到View
- 管理View元素
- 将来自Model层的数据转换为UI友好的格式
- navigation逻辑
- 管理用户UI状态
- 更多 …
承担所有这些责任,ViewController经常会变得巨大且难以维护并进行测试。
所以,现在是时候考虑改进MVC来处理这些问题了。我们称之为Model-View-Presenter MVP的改进。
MVP模式在1996年由Mike Potel首次引入,多年来曾多次讨论过。在他的文章GUI架构中,Martin Fowler讨论了这种模式,并将其与用于管理UI代码的其他模式进行了比较。 MVP有许多变体,它们之间的差别很小。在这篇文章中,我选择了目前应用程序开发中常用的常用程序。这个变体的特点是:
- MVP的视图部分由UIViews和UIViewController组成
- View委托给presenter的用户交互
- presenter包含处理用户交互的逻辑
- presenter与Model层进行通信,将数据转换为UI友好的格式,并更新视图
- presenter对UIKit没有依赖性
- 视图是passiv(转储)
以下示例将向您展示如何在操作中使用MVP。
我们的例子是一个非常简单的应用程序,显示一个简单的用户列表。 你可以从Github获得完整的源代码:https://github.com/iyadagha/iOS-mvp-sample。(Swift+OC双版本实现示例在文章末处)
让我们从用户信息的简单数据模型开始:
struct User {
let firstName: String
let lastName: String
let email: String
let age: Int
}
然后我们实现一个简单的UserService,它异步返回一个用户列表:
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。 首先我们需要用户的数据模型,可以直接从视图中使用。 它包含根据需要从视图中正确格式化的数据:
struct UserViewData{
let name: String
let age: String
}
之后,我们需要对视图进行抽象,这可以在presenter不知道UIViewController的情况下使用。 我们通过定义一个协议UserView来做到这一点:
protocol UserView: NSObjectProtocol {
func startLoading()
func finishLoading()
func setUsers(users: [UserViewData])
func setEmptyUsers()
}
该协议将在presenter中使用,稍后将从UIViewController实现。 基本上,协议包含在presenter中调用的用于控制视图的函数。
用户本身看起来像:
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)
}
}
}
}
路由将函数attachView(view:UserView)和attachView(view:UserView)用于UIViewContoller生命周期方法中的更多控制,我们将在后面看到。 请注意,将用户转换为UserViewData是presenter的责任。 另请注意,userView必须很弱以避免保留周期。
实现的最后一部分是UserViewController:
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协议。
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实现非常基本,如下所示:
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的好处之一是能够测试UI逻辑的最大部分,而无需测试UIViewController本身。 所以如果我们的presenter有一个很好的单元测试覆盖率,我们不需要再为UIViewController编写单元测试。
现在让我们来看看我们如何测试UserPresenter。 首先我们定义两个模拟工作。 一个模拟是UserService使它提供所需的用户列表。 另一个模拟是UserView来验证方法是否被正确调用。
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的行为是否正确。
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是否正常工作。
func testShouldSetEmptyIfNoUserAvailable() {
//given
let userViewMock = UserViewMock()
let userPresenterUnderTest = UserPresenter(userService: emptyUsersServiceMock)
userPresenterUnderTest.attachView(userViewMock)
//when
userPresenterUnderTest.getUsers()
//verify
XCTAssertTrue(userViewMock.setEmptyUsersCalled)
}
演变历程
我们已经看到MVP是MVC的演变。 我们只需要将UI逻辑放在一个名为Presenter的额外组件中,并使我们的UIViewController passiv(dump)成为可能。
MVP的特点之一是,presenter 和 View 都相互通信。 该视图(在本例中为UIViewController)提供了presenter的引用,反之亦然。 尽管可以使用响应式编程来删除presenter中使用的视图的参考。 通过使用ReactiveCocoa或RxSwift等响应式框架,可以构建一个体系结构,其中只有视图知道presenter,反之亦然。 在这种情况下,该架构将被称为MVVM。
😬 Contributions
- WeChat : WhatsXie
- Email : ReverseScale@iCloud.com
- Blog : https://reversescale.github.io
- Code : https://github.com/ReverseScale/MVPSimpleDemo