没有反应式编程的 MVVM

我将从一个非常简短的解释开始,在 iOS 应用程序中使用 Model-View-ViewModel(MVVM) 设计模式是什么以及为什么。当 iOS 首次出现时,Apple 建议使用 MVC(模型 - 视图 - 控制器)作为设计模式。他们在所有示例中都展示了它,并且所有第一批开发人员都乐于使用它,因为它很好地分离了业务逻辑和用户界面之间的关注点。随着应用程序变得越来越大,越来越复杂,出现了一个新的问题 - 大规模视图控制器(MVC)。因为所有业务逻辑都是在 ViewController 中添加的,所以随着时间的推移它们通常会变得太大而复杂。为了避免 MVC 问题,一种新的设计模式被引入 iOS 世界 - 模型 - 视图 - 视图模型(MVVM)模式。

StackOverflow 文档

上图显示了 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。我不会详细介绍这个类,我们可以在评论中做到这一点(使这个例子缩短)。现在让我们更新我们的 MVVMExampleViewControllerMVVMExampleViewModel 来使用这些类。

我们更新了 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,你就不会再回头了。试试吧,亲眼看看吧。