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

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

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

当你在工作中需要开发一个新的应用程序时,首先你会去考虑使用哪种设计模式,是 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,代码如下:

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

改为:

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

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

最后

赶在 11 月 1 日前,匆忙码完了这篇水文,也是对自己日常上网学习的一个总结,希望本篇文章能对大家有所帮助。 项目代码地址:github.com/ShenJieSuzh…

往期文章:

请你喝杯 ☕️ 点赞 + 关注哦~

  1. 阅读完记得给我点个赞哦,有👍 有动力
  2. 关注公众号--- HelloWorld杰少,第一时间推送新姿势

最后,创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~

分类:
iOS
标签:
分类:
iOS
标签: