沒有反應式程式設計的 MVVM
我將從一個非常簡短的解釋開始,在 iOS 應用程式中使用 Model-View-ViewModel(MVVM)
設計模式是什麼以及為什麼。當 iOS 首次出現時,Apple 建議使用 MVC(模型 - 檢視 - 控制器)作為設計模式。他們在所有示例中都展示了它,並且所有第一批開發人員都樂於使用它,因為它很好地分離了業務邏輯和使用者介面之間的關注點。隨著應用程式變得越來越大,越來越複雜,出現了一個新的問題 - 大規模檢視控制器(MVC)。因為所有業務邏輯都是在 ViewController 中新增的,所以隨著時間的推移它們通常會變得太大而複雜。為了避免 MVC 問題,一種新的設計模式被引入 iOS 世界 - 模型 - 檢視 - 檢視模型(MVVM)模式。
上圖顯示了 MVVM 的外觀。你有一個標準的 ViewController + View(在 storyboard,XIB 或 Code 中),它充當 MVVM 的 View(在後面的文字中 - View 將引用 MVVM 的 View)。檢視引用了我們的業務邏輯所在的 ViewModel。重要的是要注意 ViewModel 對 View 沒有任何瞭解,也從不對檢視有任何引用。ViewModel 具有對 Model 的引用。
這對 MVVM 的理論部分來說已經足夠了。有關它的更多資訊可以在這裡閱讀。
MVVM 的主要問題之一是當 ViewModel 沒有任何引用並且甚至不知道有關 View 的任何內容時,如何通過 ViewModel 更新 View。
這個例子的主要部分是展示如何使用 MVVM(更確切地說,如何繫結 ViewModel 和 View)而不需要任何反應式程式設計(ReactiveCocoa,ReactiveSwift 或 RxSwif)。就像一個註釋:如果你想使用 Reactive 程式設計,甚至更好,因為使用它可以很容易地完成 MVVM 繫結。但是這個例子是關於如何在沒有 Reactive 程式設計的情況下使用 MVVM。
讓我們建立一個簡單的例子來演示如何使用 MVVM。
我們的 MVVMExampleViewController
是一個簡單的 ViewController,帶有標籤和按鈕。按下按鈕時,標籤文字應設定為 Hello
。由於決定如何處理使用者使用者互動是業務邏輯的一部分,因此 ViewModel 必須決定在使用者按下按鈕時要執行的操作。MVVM 的 View 不應該做任何業務邏輯。
class MVVMExampleViewController: UIViewController {
@IBOutlet weak var helloLabel: UILabel!
var viewModel: MVVMExampleViewModel?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func sayHelloButtonPressed(_ sender: UIButton) {
viewModel?.userTriggeredSayHelloButton()
}
}
MVVMExampleViewModel
是一個簡單的 ViewModel。
class MVVMExampleViewModel {
func userTriggeredSayHelloButton() {
// How to update View's label when there is no reference to the View??
}
}
你可能想知道如何在 View 中設定 ViewModel 的引用。我通常在初始化 ViewController 時或在顯示之前執行此操作。對於這個簡單的例子,我會在 AppDelegate
中做這樣的事情:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let rootVC = window?.rootViewController as? MVVMExampleViewController {
let viewModel = MVVMExampleViewModel()
rootVC.viewModel = viewModel
}
return true
現在真正的問題是:如何在不向 ViewModel 提供 View 的情況下從 ViewModel 更新 View? (請記住,我們不會使用任何 Reactive Programming iOS 庫)
你可以考慮使用 KVO,但這會讓事情變得太複雜。一些聰明的人已經考慮過這個問題,並提出了邦德庫 。該庫可能看起來很複雜,起初有點難以理解,所以我只需要它的一小部分,使我們的 MVVM 完全正常執行。
讓我們來介紹 Dynamic
類,它是我們簡單但功能齊全的 MVVM 模式的核心。
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
func bind(_ listener: Listener?) {
self.listener = listener
}
func bindAndFire(_ listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init(_ v: T) {
value = v
}
}
Dynamic
類使用 Generics 和 Closures 將我們的 ViewModel 繫結到 View。我不會詳細介紹這個類,我們可以在評論中做到這一點(使這個例子縮短)。現在讓我們更新我們的 MVVMExampleViewController
和 MVVMExampleViewModel
來使用這些類。
我們更新了 MVVMExampleViewController
class MVVMExampleViewController: UIViewController {
@IBOutlet weak var helloLabel: UILabel!
var viewModel: MVVMExampleViewModel?
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
func bindViewModel() {
if let viewModel = viewModel {
viewModel.helloText.bind({ (helloText) in
DispatchQueue.main.async {
// When value of the helloText Dynamic variable
// is set or changed in the ViewModel, this code will
// be executed
self.helloLabel.text = helloText
}
})
}
}
@IBAction func sayHelloButtonPressed(_ sender: UIButton) {
viewModel?.userTriggeredSayHelloButton()
}
}
更新了 MVVMExampleViewModel
:
class MVVMExampleViewModel {
// we have to initialize the Dynamic var with the
// data type we want
var helloText = Dynamic("")
func userTriggeredSayHelloButton() {
// Setting the value of the Dynamic variable
// will trigger the closure we defined in the View
helloText.value = "Hello"
}
}
這就對了。你的 ViewModel
現在能夠更新 View
而不需要參考 View
。
這是一個非常簡單的例子,但我想你已經知道這有多強大了。我不會詳細介紹 MVVM 的優點,但是一旦從 MVC 切換到 MVVM,你就不會再回頭了。試試吧,親眼看看吧。