[譯] MVVM-C 與 Swift


MVVM-C 與 Swift

簡介

現今,iOS 開發者面臨的最大挑戰是構建一個健壯的應用程序,它必須易於維護、測試和擴展。javascript

在這篇文章裏,你會學到一種可靠的方法來達到目的。html

首先,簡要介紹下你即將學習的內容:
架構模式.前端

架構模式

它是什麼

架構模式是給定上下文中軟件體系結構中常見的,可重用的解決方案。架構與軟件設計模式類似,但涉及的範圍更廣。架構解決了軟件工程中的各類問題,如計算機硬件性能限制,高可用性和最小化業務風險。一些架構模式已經在軟件框架內實現。java

摘自 Wikipediareact

在你開始一個新項目或功能的時候,你須要花一些時間來思考架構模式的使用。經過一個透徹的分析,你能夠避免耗費不少天的時間在重構一個混亂的代碼庫上。android

主要的模式

在項目中,有幾種可用的架構模式,而且你能夠在項目中使用多個,由於每一個模式都能更好地適應特定的場景。ios

當你閱讀這幾種模式時,主要會遇到:git

Model-View-Controller

這是最多見的,也許在你的第一個 iOS 應用中已經使用過。不幸地是,這也是最糟糕的模式,由於 Controller 不得無論理每個依賴(API、數據庫等等),包括你應用的業務邏輯,並且與 UIKit 的耦合度很高,這意味着很難去測試。github

你應該避免這種模式,用下面的某種來代替它。數據庫

Model-View-Presenter

這是第一個 MVC 模式的備選方案之一,一次對 ControllerView 之間解耦的很好的嘗試。

在 MVP 中,你有一層叫作 Presenter 的新結構來處理業務邏輯。而 View —— 你的 UIViewController 以及任何 UIKit 組件,都是一個笨的對象,他們只經過 Presenter 更新,並在 UI 事件被觸發的時候,負責通知 Presenter。因爲 Presenter 沒有任何 UIKit 的引用,因此很是容易測試。

Viper

這是 Bob 叔叔的清晰架構的表明。

這種模式的強大之處在於,它合理分配了不一樣層次之間的職責。經過這種方式,你的每一個層次作的的事變得不多,易於測試,而且具有單一職責。這種模式的問題是,在大多數場合裏,它過於複雜。你須要管理不少層,這會讓你感到混亂,難於管理。

這種模式並不容易掌握,你能夠在這裏找到關於這種架構模式更詳細的文章。

Model-View-ViewModel

最後但也是最重要的,MVVM 是一個相似於 MVP 的框架,由於層級結構幾乎相同。你能夠認爲 MVVM 是 MVP 版本的一個進化,而這得益於 UI 綁定。

UI 綁定是在 ViewViewModel 之間創建一座單向或雙向的橋樑,而且二者之間以一種很是透明地方式進行溝通。

不幸地是,iOS 沒有原生的方式來實現,因此你必須經過三方庫/框架或者本身寫一個來達成目的。

在 Swift 裏有多種方式實現 UI 綁定:

RxSwift (或 ReactiveCocoa)

RxSwiftReactiveX 家族的一個 Swift 版本的實現。一旦你掌握了它,你就能很輕鬆地切換到 RxJava、RxJavascript 等等。

這個框架容許你來用函數式(FRP)的方式來編寫程序,而且因爲內部庫 RxCocoa,你能夠輕鬆實現 ViewViewModel 之間的綁定:

class ViewController: UIViewController {

    @IBOutlet private weak var userLabel: UILabel!

    private let viewModel: ViewModel
    private let disposeBag: DisposeBag

    private func bindToViewModel() {
        viewModel.myProperty
            .drive(userLabel.rx.text)
            .disposed(by: disposeBag)
    }
}複製代碼

我不會解釋如何完全地使用 RxSwift,由於這超出本文的目標,它本身會有文章來解釋。

FRP 讓你學習到了一種新的方式來開發,你可能對它或愛或恨。若是你沒用過 FRP 開發,那你須要花費幾個小時來熟悉和理解如何正確地使用它,由於它是一個徹底不一樣的編程概念。

另外一個相似於 RxSwift 的框架是 ReactiveCocoa,若是你想了解他們之間主要的區別的話,你能夠看看這篇文章

代理

若是你想避免導入並學習新的框架,你可使用代理做爲替代。不幸地是,使用這種方法,你將失去透明綁定的功能,由於你必須手動綁定。這個版本的 MVVM 很是相似於 MVP。

這種方式的策略是經過 View 內部的 ViewModel 保持一個對代理實現的引用。這樣 ViewModel 就能在無需引用任何 UIKit 對象的狀況下更新 View

這有個例子:

class ViewController: UIViewController, ViewModelDelegate {

    @IBOutlet private weak var userLabel: UILabel?

    private let viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
        viewModel.delegate = self
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func userNameDidChange(text: String) {
        userLabel?.text = text
    }
}


protocol ViewModelDelegate: class {
    func userNameDidChange(text: String)
}

class ViewModel {

    private var userName: String {
        didSet {
            delegate?.userNameDidChange(text: userName)
        }
    }
    weak var delegate: ViewModelDelegate? {
        didSet {
            delegate?.userNameDidChange(text: userName)
        }
    }

    init() {
        userName = "I 💚 hardcoded values"
    }
}複製代碼

閉包

和代理很是類似,不過不一樣的是,你使用的是閉包來代替代理。

閉包是 ViewModel 的屬性,而 View 使用它們來更新 UI。你必須注意在閉包裏使用 [weak self],避免形成循環引用。

關於 Swift 閉包的循環引用,你能夠閱讀這篇文章

這有一個例子:

class ViewController: UIViewController {

    @IBOutlet private weak var userLabel: UILabel?

    private let viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
        viewModel.userNameDidChange = { [weak self] (text: String) in
            self?.userNameDidChange(text: text)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func userNameDidChange(text: String) {
        userLabel?.text = text
    }
}

class ViewModel {

    var userNameDidChange: ((String) -> Void)? {
        didSet {
            userNameDidChange?(userName)
        }
    }

    private var userName: String {
        didSet {
            userNameDidChange?(userName)
        }
    }

    init() {
        userName = "I 💚 hardcoded values"
    }
}複製代碼

抉擇: MVVM-C

在你不得不選擇一個架構模式時,你須要理解哪種更適合你的需求。在這些模式裏,MVVM 是最好的選擇之一,由於它強大的同時,也易於使用。

不幸地是這種模式並不完美,主要的缺陷是 MVVM 沒有路由管理。

咱們要添加一層新的結構,來讓它得到 MVVM 的特性,而且具有路由的功能。因而它就變成了:Model-View-ViewModel-Coordinator (MVVM-C)

示例的項目會展現 Coordinator 如何工做,而且如何管理不一樣的層次。

入門

你能夠在這裏下載項目源碼。

這個例子被簡化了,以便於你能夠專一於 MVVM-C 是如何工做的,所以 GitHub 上的類可能會有輕微出入。

示例應用是一個普通的儀表盤應用,它從公共 API 獲取數據,一旦數據準備就緒,用戶就能夠經過 ID 查找實體,以下面的截圖:

應用程序有不一樣的方式來添加視圖控制器,因此你會看到,在有子視圖控制器的邊緣案例中,如何使用 Coordinator

MVVM-C 的層級結構

Coordinator

它的職責是顯示一個新的視圖,並注入 ViewViewModel 所須要的依賴。

Coordinator 必須提供一個 start 方法,來建立 MVVM 層次而且添加 View 到視圖的層級結構中。

你可能會常常有一組 Coordinator 子類,由於在你當前的視圖中,可能會有子視圖,就像咱們的例子同樣:

final class DashboardContainerCoordinator: Coordinator {

    private var childCoordinators = [Coordinator]()

    private weak var dashboardContainerViewController: DashboardContainerViewController?
    private weak var navigationController: UINavigationControllerType?

    private let disposeBag = DisposeBag()

    init(navigationController: UINavigationControllerType) {
        self.navigationController = navigationController
    }

    func start() {
        guard let navigationController = navigationController else { return }
        let viewModel = DashboardContainerViewModel()
        let container = DashboardContainerViewController(viewModel: viewModel)

        bindShouldLoadWidget(from: viewModel)

        navigationController.pushViewController(container, animated: true)

        dashboardContainerViewController = container
    }

    private func bindShouldLoadWidget(from viewModel: DashboardContainerViewModel) {
        viewModel.rx_shouldLoadWidget.asObservable()
            .subscribe(onNext: { [weak self] in
                self?.loadWidgets()
            })
            .addDisposableTo(disposeBag)
    }

    func loadWidgets() {
        guard let containerViewController = usersContainerViewController() else { return }
        let coordinator = UsersCoordinator(containerViewController: containerViewController)
        coordinator.start()

        childCoordinators.append(coordinator)
    }

    private func usersContainerViewController() -> ContainerViewController? {
        guard let dashboardContainerViewController = dashboardContainerViewController else { return nil }

        return ContainerViewController(parentViewController: dashboardContainerViewController,
                                       containerView: dashboardContainerViewController.usersContainerView)
    }
}複製代碼

你必定能注意到在 Coordinator 裏,一個父類 UIViewController 對象或者子類對象,好比 UINavigationController,被注入到構造器之中。由於 Coordinator 有責任添加 View 到視圖層級之中,它必須知道那個父類添加了 View

在上面的例子裏,DashboardContainerCoordinator 實現了協議 Coordinator

protocol Coordinator {
    func start()
}複製代碼

這便於你使用多態)。

建立完第一個 Coordinator 後,你必須把它做爲程序的入口放到 AppDelegate 中:

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    private let navigationController: UINavigationController = {
        let navigationController = UINavigationController()
        navigationController.navigationBar.isTranslucent = false
        return navigationController
    }()

    private var mainCoordinator: DashboardContainerCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow()
        window?.rootViewController = navigationController
        let coordinator = DashboardContainerCoordinator(navigationController: navigationController)
        coordinator.start()
        window?.makeKeyAndVisible()

        mainCoordinator = coordinator

        return true
    }
}複製代碼

AppDelegate 裏,咱們實例化一個新的 DashboardContainerCoordinator,經過 start 方法,咱們把新的視圖推入 navigationController 裏。

你能夠看到在 GitHub 上的項目是如何注入一個 UINavigationController 類型的對象,並去除 UIKitCoordinator 之間的耦合。

Model

Model 表明數據。它必須儘量的簡潔,沒有業務邏輯。

struct UserModel: Mappable {
    private(set) var id: Int?
    private(set) var name: String?
    private(set) var username: String?

    init(id: Int?, name: String?, username: String?) {
        self.id = id
        self.name = name
        self.username = username
    }

    init?(map: Map) { }

    mutating func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
        username <- map["username"]
    }
}複製代碼

實例項目使用開源庫 ObjectMapper 將 JSON 轉換爲對象。

ObjectMapper 是一個使用 Swift 編寫的框架。它能夠輕鬆的讓你在 JSON 和模型對象(類和結構體)之間相互轉換。

在你從 API 得到一個 JSON 響應的時候,它會很是有用,由於你必須建立模型對象來解析 JSON 字符串。

View

View 是一個 UIKit 對象,就像 UIViewController 同樣。

它一般持有一個 ViewModel 的引用,經過 Coordinator 注入來建立綁定。

final class DashboardContainerViewController: UIViewController {

    let disposeBag = DisposeBag()

    private(set) var viewModel: DashboardContainerViewModelType

    init(viewModel: DashboardContainerViewModelType) {
        self.viewModel = viewModel

        super.init(nibName: nil, bundle: nil)

        configure(viewModel: viewModel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(viewModel: DashboardContainerViewModelType) {
        viewModel.bindViewDidLoad(rx.viewDidLoad)

        viewModel.rx_title
            .drive(rx.title)
            .addDisposableTo(disposeBag)
    }
}複製代碼

在這個例子中,視圖控制器中的標題被綁定到 ViewModelrx_title 屬性上。這樣在 ViewModel 更新 rx_title 值的時候,視圖控制器中的標題就會根據新的值自動更新。

ViewModel

ViewModel 是這種架構模式的核心層。它的職責是保持 ViewModel 的更新。因爲業務邏輯在這個類中,你須要用不一樣的組件的單一職責來保證 ViewModel 儘量的乾淨。

final class UsersViewModel {

    private var dataProvider: UsersDataProvider
    private var rx_usersFetched: Observable<[UserModel]>

    lazy var rx_usersCountInfo: Driver<String> = {
        return UsersViewModel.createUsersCountInfo(from: self.rx_usersFetched)
    }()
    var rx_userFound: Driver<String> = .never()

    init(dataProvider: UsersDataProvider) {
        self.dataProvider = dataProvider

        rx_usersFetched = dataProvider.fetchData(endpoint: "http://jsonplaceholder.typicode.com/users")
            .shareReplay(1)
    }

    private static func createUsersCountInfo(from usersFetched: Observable<[UserModel]>) -> Driver<String> {
        return usersFetched
            .flatMapLatest { users -> Observable<String> in
                return .just("The system has \(users.count) users")
            }
            .asDriver(onErrorJustReturn: "")
    }
}複製代碼

在這個例子中,ViewModel 有一個在構造器中注入的數據提供者,它用於從公共 API 中獲取數據。一旦數據提供者返回了取得的數據,ViewModel 就會經過 rx_usersCountInfo 發射一個新用戶數量相關的新事件。由於綁定了觀察者 rx_usersCountInfo,這個新事件會被髮送給 View,而後更新 UI。

可能會有不少不一樣的組件在你的 ViewModel 裏,好比一個用來管理數據庫(CoreData、Realm 等等)的數據控制器,一個用來與你 API 和其餘任何外部依賴交互的數據提供者。

由於全部 ViewModel 都使用了 RxSwift,因此當一個屬性是 RxSwift 類型(DriverObservable 等等)的時候,就會有一個 rx_ 前綴。這不是強制的,只是它能夠幫助你更好的識別哪些屬性是 RxSwift 對象。

結論

MVVM-C 有不少優勢,能夠提升應用程序的質量。你應該注意使用哪一種方式來進行 UI 綁定,由於 RxSwift 不容易掌握,並且若是你不明白你作的是什麼,調試和測試有時可能會有點棘手。

個人建議是一點點地開始使用這種架構模式,這樣你能夠學習不一樣層次的使用,而且能保證層次之間的良好的分離,易於測試。

FAQ

MVVM-C 有什麼限制嗎?

是的,固然有。若是你正作一個複雜的項目,你可能會遇到一些邊緣案例,MVVM-C 可能沒法使用,或者在一些小功能上使用過分。若是你開始使用 MVVM-C,並不意味着你必須在每一個地方都強制的使用它,你應該始終選擇更適合你需求的架構。

我能用 RxSwift 同時使用函數式和命令式編程嗎?

是的,你能夠。可是我建議你在遺留的代碼中保持命令式的方式,而在新的實現裏使用函數式編程,這樣你能夠利用 RxSwift 強大的優點。若是你使用 RxSwift 僅僅爲了 UI 綁定,你能夠輕鬆使用命令式編寫程序,而只用函數響應式編程來設置綁定。

我能夠在企業項目中使用 RxSwift 嗎?

這取決於你要開新項目,仍是要維護舊代碼。在有遺留代碼的項目中,你可能沒法使用 RxSwift,由於你須要重構不少的類。若是你有時間和資源來作,我建議你新開一項目一點一點的作,不然仍是嘗試其餘的方法來解決 UI 綁定的問題。

須要考慮的一個重要事情是,RxSwift 最終會成爲你項目中的另外一個依賴,你可能會由於 RxSwift 的破壞性改動而致使浪費時間的風險,或者缺乏要在邊緣案例中實現功能的文檔。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索