用一個簡單的使用者列表介面展示:在iOS中用使用 MVP (翻譯)

WhatsXie發表於2018-03-29

原文:《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之間的粘合劑。

用一個簡單的使用者列表介面展示:在iOS中用使用 MVP (翻譯)

這種模式的一個很好的部分是將業務邏輯和業務規則封裝在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(轉儲)

用一個簡單的使用者列表介面展示:在iOS中用使用 MVP (翻譯)

以下示例將向您展示如何在操作中使用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
    }
}
複製程式碼

用一個簡單的使用者列表介面展示:在iOS中用使用 MVP (翻譯)

單元測試

做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

相關文章