闭包是个好东西,巧用闭包实现数据绑定

2022-08-04 14:22:15 浏览数 (1)

前言

当你在工作中需要开发一个新的应用程序时,首先你会去考虑使用哪种设计模式,是 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)
    }
}
  1. typealias 关键字是声明一个别名,我们将 (T) -> Void 这一闭包取名为 Listener;
  2. Box 类里定义一个 Listener 属性:listener;
  3. Box 类里定义了一个泛型属性 value 并用 didSet 属性观察器检测有没有值发生改变,如果发生了改变,则通知 Listener 更新值;
  4. 当 Listener 在 Box 上调用 bind(listener:) 时,它会变成 Listener 并立即收到 Box 的当前值的通知;

案例实践

在本次的演示中,我拿了之前的一个项目代码做参考,此项目也是我之前写的一篇文章 “iOS 优雅的处理网络数据,你真的会吗?不如看看这篇” 调研写的代码。

简单的描述一下需求:我们需要将在 ViewModel 中通过网络异步获取到图片数据并返回给主视图里的 TableView, 并将数据加载出来。

原先在这个项目中,我通过 Delegate 的方式去实现数据回调并刷新,代码如下:

  1. 定义 PreloadCellViewModelDelegate 协议,用于回调
代码语言:javascript复制
protocol PreloadCellViewModelDelegate: NSObject {
    func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?)
    func onFetchFailed(with reason: String)
}
  1. 定义数据源
代码语言:javascript复制
private var images: [ImageModel] = []
  1. 获取异步数据后,调用协议里的方法,回调数据然后进行 UI 刷新
代码语言:javascript复制
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)
                }
            }
        }
    }
  1. 在主视图中刷新数据
代码语言:javascript复制
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()
    }
}

但是现在我觉得这并不是很优雅,于是乎我就修改了一下代码,利用闭包的方式实现数据绑定。

  1. 将 ViewModel 中需要对外的数据源的代码由
代码语言:javascript复制
private var images: [ImageModel] = []

改为:

代码语言:javascript复制
var images: Box<[ImageModel]> = Box([])
  1. 异步获取图片数据时,就不需要调用协议里的方法了,直接修改 images 数组的值,就会触发属性观察器,代码如下:
代码语言:javascript复制
    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)
            }
        }
    }
  1. 在主视图中调用 bind 函数,来绑定 ViewModel, 代码如下:
代码语言:javascript复制
viewModel.images.bind { [weak self] _ in
            guard let strongSelf = self else {
                return
            }
            strongSelf.tableView.reloadData()
        }

这样,我们就利用闭包完成了数据绑定,相比使用 Delegate,是不是在代码上简洁了不少,代码一下子就优雅了起来。

最后

匆忙码完了这篇水文,也是对自己日常上网学习的一个总结,希望本篇文章能对大家有所帮助。

0 人点赞