前言
当你在工作中需要开发一个新的应用程序时,首先你会去考虑使用哪种设计模式,是 MVC 呢还是 MVVM?当然这话放在今儿个说,大家肯定会一致的选择 MVVM,因为相比 MVC 模式,MVVM 模式有太多的优势,譬如说移除了在 View Controller 中的业务逻辑,将这部分代码放在 View Model 中执行,职责分工明确等等。
数据绑定
但是,说到 MVVM 模式的时候,我们又必须讲到数据绑定这个知识点。以往我们再处理异步数据的时候,往往都会通过 Delegate 或者 Notification 等方式,待收到异步数据后再去刷新 UI。这样处理数据并没有毛病,但是如果遇到 UI 上有大量的控件需要不定时更新数据时,那通过 Delegate 和 Notification 的方式就会显得不够优雅,所以我们才会讲到数据绑定这个知识。
现在关于数据绑定的成熟解决方案有很多,譬如说 RXSwift,KVO 等等,在这里我就不再多介绍这些方式了,感兴趣的同学可以自行 Google 一下。今天我要给大家介绍的是另一种方式,那就是使用闭包来实现数据绑定。
闭包为何物
闭包是自包含的函数代码块,可以在代码中被传递和使用。闭包可以捕获和存储其所在上下文中任意的常量或变量的引用。你可以将闭包作为一个函数的参数,也可以将它作为函数的返回值。
以上就是我在网上搜到的关于闭包的解释,按我的理解,闭包就是一个可执行的代码块,可用作参数传入。
创建 Box 类
好了,不说这么多的废话了,接下来咱们就直接开始编码。
首先,为了能让 ViewModel 和 View 之间能形成绑定,我们需要提供一种简单的机制让 ViewModel 中的数据源与 View 中的控件绑定在一起。这里我用到的一种方式叫 Boxing, 这也是我阅读别人代码时看到的,觉得非常好,它使用属性观察器的机制,一旦值发生改变,则会通知观察者值已经改变了。
创建一个类,名叫 Box,代码如下:
代码语言:javascript复制import Foundation
final class Box<T> {
// 声明一个别名
typealias Listener = (T) -> Void
var listener: Listener?
var value: T {
didSet {
listener?(value)
}
}
init(_ value: T){
self.value = value
}
func bind(listener: Listener?) {
self.listener = listener
listener?(value)
}
}
- typealias 关键字是声明一个别名,我们将 (T) -> Void 这一闭包取名为 Listener;
- Box 类里定义一个 Listener 属性:listener;
- Box 类里定义了一个泛型属性 value 并用 didSet 属性观察器检测有没有值发生改变,如果发生了改变,则通知 Listener 更新值;
- 当 Listener 在 Box 上调用 bind(listener:) 时,它会变成 Listener 并立即收到 Box 的当前值的通知;
案例实践
在本次的演示中,我拿了之前的一个项目代码做参考,此项目也是我之前写的一篇文章 “iOS 优雅的处理网络数据,你真的会吗?不如看看这篇” 调研写的代码。
简单的描述一下需求:我们需要将在 ViewModel 中通过网络异步获取到图片数据并返回给主视图里的 TableView, 并将数据加载出来。
原先在这个项目中,我通过 Delegate 的方式去实现数据回调并刷新,代码如下:
- 定义 PreloadCellViewModelDelegate 协议,用于回调
protocol PreloadCellViewModelDelegate: NSObject {
func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?)
func onFetchFailed(with reason: String)
}
- 定义数据源
private var images: [ImageModel] = []
- 获取异步数据后,调用协议里的方法,回调数据然后进行 UI 刷新
func fetchImages() {
guard !isFetchInProcess else {
return
}
isFetchInProcess = true
// 延时 2s 模拟网络环境
print(" 模拟网络数据请求 ")
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() 2) {
print(" 模拟网络数据请求返回成功 ")
DispatchQueue.main.async {
self.total = 1000
self.currentPage = 1
self.isFetchInProcess = false
// 初始化 30个 图片
let imagesData = (1...30).map {
ImageModel(url: baseURL "($0).png", order: $0)
}
self.images.append(contentsOf: imagesData)
if self.currentPage > 1 {
let newIndexPaths = self.calculateIndexPathsToReload(from: imagesData)
self.delegate?.onFetchCompleted(with: newIndexPaths)
} else {
self.delegate?.onFetchCompleted(with: .none)
}
}
}
}
- 在主视图中刷新数据
extension ViewController: PreloadCellViewModelDelegate {
func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
guard let newIndexPathsToReload = newIndexPathsToReload else {
tableView.tableFooterView = nil
tableView.reloadData()
return
}
let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
indicatorView.stopAnimating()
tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}
func onFetchFailed(with reason: String) {
indicatorView.stopAnimating()
tableView.reloadData()
}
}
但是现在我觉得这并不是很优雅,于是乎我就修改了一下代码,利用闭包的方式实现数据绑定。
- 将 ViewModel 中需要对外的数据源的代码由
private var images: [ImageModel] = []
改为:
代码语言:javascript复制var images: Box<[ImageModel]> = Box([])
- 异步获取图片数据时,就不需要调用协议里的方法了,直接修改 images 数组的值,就会触发属性观察器,代码如下:
func fetchImages() {
guard !isFetchInProcess else {
return
}
isFetchInProcess = true
// 延时 2s 模拟网络环境
print(" 模拟网络数据请求 ")
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() 2) {
print(" 模拟网络数据请求返回成功 ")
DispatchQueue.main.async {
self.total = 1000
self.currentPage = 1
self.isFetchInProcess = false
// 初始化 30个 图片
let imagesData = (1...30).map {
ImageModel(url: baseURL "($0).png", order: $0)
}
self.images.value.append(contentsOf: imagesData)
}
}
}
- 在主视图中调用 bind 函数,来绑定 ViewModel, 代码如下:
viewModel.images.bind { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.tableView.reloadData()
}
这样,我们就利用闭包完成了数据绑定,相比使用 Delegate,是不是在代码上简洁了不少,代码一下子就优雅了起来。
最后
匆忙码完了这篇水文,也是对自己日常上网学习的一个总结,希望本篇文章能对大家有所帮助。