- 原文地址:MVVM-C with Swift
- 原文做者:Marco Santarossa
- 譯文出自:掘金翻譯計劃
- 譯者:Deepmissea
- 校對者:atuooo,1992chenlu
現今,iOS 開發者面臨的最大挑戰是構建一個健壯的應用程序,它必須易於維護、測試和擴展。javascript
在這篇文章裏,你會學到一種可靠的方法來達到目的。html
首先,簡要介紹下你即將學習的內容:
架構模式.前端
架構模式是給定上下文中軟件體系結構中常見的,可重用的解決方案。架構與軟件設計模式類似,但涉及的範圍更廣。架構解決了軟件工程中的各類問題,如計算機硬件性能限制,高可用性和最小化業務風險。一些架構模式已經在軟件框架內實現。java
摘自 Wikipedia。react
在你開始一個新項目或功能的時候,你須要花一些時間來思考架構模式的使用。經過一個透徹的分析,你能夠避免耗費不少天的時間在重構一個混亂的代碼庫上。android
在項目中,有幾種可用的架構模式,而且你能夠在項目中使用多個,由於每一個模式都能更好地適應特定的場景。ios
當你閱讀這幾種模式時,主要會遇到:git
這是最多見的,也許在你的第一個 iOS 應用中已經使用過。不幸地是,這也是最糟糕的模式,由於 Controller
不得無論理每個依賴(API、數據庫等等),包括你應用的業務邏輯,並且與 UIKit
的耦合度很高,這意味着很難去測試。github
你應該避免這種模式,用下面的某種來代替它。數據庫
這是第一個 MVC 模式的備選方案之一,一次對 Controller
和 View
之間解耦的很好的嘗試。
在 MVP 中,你有一層叫作 Presenter
的新結構來處理業務邏輯。而 View
—— 你的 UIViewController
以及任何 UIKit
組件,都是一個笨的對象,他們只經過 Presenter
更新,並在 UI 事件被觸發的時候,負責通知 Presenter
。因爲 Presenter
沒有任何 UIKit
的引用,因此很是容易測試。
這是 Bob 叔叔的清晰架構的表明。
這種模式的強大之處在於,它合理分配了不一樣層次之間的職責。經過這種方式,你的每一個層次作的的事變得不多,易於測試,而且具有單一職責。這種模式的問題是,在大多數場合裏,它過於複雜。你須要管理不少層,這會讓你感到混亂,難於管理。
這種模式並不容易掌握,你能夠在這裏找到關於這種架構模式更詳細的文章。
最後但也是最重要的,MVVM 是一個相似於 MVP 的框架,由於層級結構幾乎相同。你能夠認爲 MVVM 是 MVP 版本的一個進化,而這得益於 UI 綁定。
UI 綁定是在 View
和 ViewModel
之間創建一座單向或雙向的橋樑,而且二者之間以一種很是透明地方式進行溝通。
不幸地是,iOS 沒有原生的方式來實現,因此你必須經過三方庫/框架或者本身寫一個來達成目的。
在 Swift 裏有多種方式實現 UI 綁定:
RxSwift 是 ReactiveX 家族的一個 Swift 版本的實現。一旦你掌握了它,你就能很輕鬆地切換到 RxJava、RxJavascript 等等。
這個框架容許你來用函數式(FRP)的方式來編寫程序,而且因爲內部庫 RxCocoa,你能夠輕鬆實現 View
和 ViewModel
之間的綁定:
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 是最好的選擇之一,由於它強大的同時,也易於使用。
不幸地是這種模式並不完美,主要的缺陷是 MVVM 沒有路由管理。
咱們要添加一層新的結構,來讓它得到 MVVM 的特性,而且具有路由的功能。因而它就變成了:Model-View-ViewModel-Coordinator (MVVM-C)
示例的項目會展現 Coordinator
如何工做,而且如何管理不一樣的層次。
你能夠在這裏下載項目源碼。
這個例子被簡化了,以便於你能夠專一於 MVVM-C 是如何工做的,所以 GitHub 上的類可能會有輕微出入。
示例應用是一個普通的儀表盤應用,它從公共 API 獲取數據,一旦數據準備就緒,用戶就能夠經過 ID 查找實體,以下面的截圖:
應用程序有不一樣的方式來添加視圖控制器,因此你會看到,在有子視圖控制器的邊緣案例中,如何使用 Coordinator
。
它的職責是顯示一個新的視圖,並注入 View
和 ViewModel
所須要的依賴。
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
類型的對象,並去除 UIKit
和 Coordinator
之間的耦合。
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
是一個 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)
}
}複製代碼
在這個例子中,視圖控制器中的標題被綁定到 ViewModel
的 rx_title
屬性上。這樣在 ViewModel
更新 rx_title
值的時候,視圖控制器中的標題就會根據新的值自動更新。
ViewModel
是這種架構模式的核心層。它的職責是保持 View
和 Model
的更新。因爲業務邏輯在這個類中,你須要用不一樣的組件的單一職責來保證 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 類型(Driver
、Observable
等等)的時候,就會有一個 rx_
前綴。這不是強制的,只是它能夠幫助你更好的識別哪些屬性是 RxSwift 對象。
MVVM-C 有不少優勢,能夠提升應用程序的質量。你應該注意使用哪一種方式來進行 UI 綁定,由於 RxSwift 不容易掌握,並且若是你不明白你作的是什麼,調試和測試有時可能會有點棘手。
個人建議是一點點地開始使用這種架構模式,這樣你能夠學習不一樣層次的使用,而且能保證層次之間的良好的分離,易於測試。
MVVM-C 有什麼限制嗎?
是的,固然有。若是你正作一個複雜的項目,你可能會遇到一些邊緣案例,MVVM-C 可能沒法使用,或者在一些小功能上使用過分。若是你開始使用 MVVM-C,並不意味着你必須在每一個地方都強制的使用它,你應該始終選擇更適合你需求的架構。
我能用 RxSwift 同時使用函數式和命令式編程嗎?
是的,你能夠。可是我建議你在遺留的代碼中保持命令式的方式,而在新的實現裏使用函數式編程,這樣你能夠利用 RxSwift 強大的優點。若是你使用 RxSwift 僅僅爲了 UI 綁定,你能夠輕鬆使用命令式編寫程序,而只用函數響應式編程來設置綁定。
我能夠在企業項目中使用 RxSwift 嗎?
這取決於你要開新項目,仍是要維護舊代碼。在有遺留代碼的項目中,你可能沒法使用 RxSwift,由於你須要重構不少的類。若是你有時間和資源來作,我建議你新開一項目一點一點的作,不然仍是嘗試其餘的方法來解決 UI 綁定的問題。
須要考慮的一個重要事情是,RxSwift 最終會成爲你項目中的另外一個依賴,你可能會由於 RxSwift 的破壞性改動而致使浪費時間的風險,或者缺乏要在邊緣案例中實現功能的文檔。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。