SwiftUI - 使用 Combine 框架来构建 ViewModels

1,506 阅读3分钟

本文翻译自 Building ViewModels with Combine framework

之前我曾在 一篇文章 中讨论过 MVVM 模式,我仍然会在一些老的 UIKit 项目中使用这一概念。但是我觉得是时候重写这篇文章了。这周我们会讨论关于如何使用 Combine framework 来构建响应式的 ViewController

什么是 ViewModel?

ViewModel 是 View 和数据之间的一个层,是 View 的数据化表示。ViewModels 通常会通过一些 service 对象来拉取数据,将数据格式化,再将这些数据提供给你的 View。

你可以通过 以前的这篇文章 来了解 MVVM 模式。

苹果开始提倡 MVVM 模式

我最近注意到一些有趣的事情,苹果开始把 ObservableObject 协议移动到了 Combine framework 当中。看起来苹果开始提倡使用 MVVM 模式了。让我们先来看下 ObservableObject 协议长啥样:

/// 表示一种对象的类型,这种对象持有一个可以在对象发生改变前发出事件的 publisher
public protocol ObservableObject : AnyObject {

    /// 定义 publisher 类型,publisher 类型的对象将在 ObservableObject 发生改变前发出事件
    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never

    /// publisher 实例,当对象发生改变前,它会发出事件
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}

ObservableObject 协议只有一个要求,就是要提供一个在对象发生改变发出事件的 publisher。以下是一个实现 ObservableObject 协议的例子:

final class PostsViewModel: ObservableObject {
    let objectWillChange = PassthroughSubject<Void, Never>()

    private (set) var posts: [Post] = []

    func fetch() {
        // 拉取数据
        
        // publisher 发出事件
        objectWillChange.send()
        
        // 把拉取到的数据赋值给 posts 成员
    }
}

以上例子中的 ViewModel 可以拉取 post,并将其保存在变量当中,他会通过 objectWillChange publisher 来发送事件给外部。接着我们编写这个 ViewModel 所对应的 ViewController :

final class PostsViewController: UIViewController {
    let viewModel: PostsViewModel

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.fetch()
    }

    private func bindViewModel() {
        viewModel.objectWillChange.sink { [weak self] in
            guard let self = self else {
                return
            }
            self.renderPosts(self.viewModel.posts)
        }
    }
}

以上例子中,我们使用 PostsViewController 来观察 ViewModel 的变化,并且触发 ViewModel 的数据拉取。一旦 ViewModel 拉取到了数据,他就会通过 publisher 发出事件,随后 ViewController 就会调用 renderPosts 方法来展示下载到的 posts 数据。

@Published 属性包装器

@Published 属性包装器可以让我们把任意属性包装到 publisher 当中,只要这些属性发生改变,publisher 就会发出事件。

有了 @Published 包装器之后,你都不用再定义 objectWillChange publisher 了。Swift 编译器会自动生成 objectWillChange,一旦 @Published 所包装的属性发生变化,就会发生事件出去。以下是使用 @Published 重构后的 ViewModel 代码:

final class PostsViewModel: ObservableObject {
    @Published private(set) var posts: [Post] = []

    func fetch() {
        // fetch posts and assign them to `posts` variable
    }
}

正如以上例子所展示的,我们不再需要自己定义 objectWillChange publisher,Swift 编译器会帮我们自动生成。我们之前写好的 PostsViewController 代码也不需要做任何改动。

正如上面所展示的,@Published 属性包装器会将我们的属性包装到 publisher 当中。在这之前我们的 PostsViewController 需要通过 objectWillChange 来获知属性的变化,现在不需要这么做了:

final class PostsViewController: UIViewController {
    let viewModel: PostsViewModel

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.fetch()
    }

    private func bindViewModel() {
        viewModel.$posts.sink { [weak self] posts in
            self?.renderPosts(posts)
        }
    }
}

以上是重构后的例子,在 bindViewModel 方法中,我们订阅了 $posts 属性,仅当我们指定的属性发生变化时,我们才会更新界面。当 ViewModel 中的属性越来越多的时候,这种用法会非常方便。

苹果在 WWDC 19 的 Mastering Xcode Previews 章节中也讨论了 ViewModels 模式。

总结

我们也可以使用 RxSwift, ReactiveSwift 或者其他类似的响应式框架来实现类似的逻辑。但我觉得 MVVM 模式正成为构建 iOS app 的默认选择。到目前为止,苹果也已经给我们提供了使用这一框架的所有工具。