没有反应式编程的 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,你就不会再回头了。试试吧,亲眼看看吧。