用一个简单的用户列表界面展示:在iOS中用使用 MVP (翻译)

545 阅读5分钟

原文:《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