BackingStore 0.1.7

BackingStore 0.1.7

Patrick Lynch 维护。



  • patricklynch

BackingStore Logo

CI Status Version Carthage compatible License Platform Language

BackingStore是一个框架,它自动处理表格视图和收集视图中的完美批量更新。这是通过提供一种存储基于要显示在UICollectionViewUITableView中可见项的表模型的方法来实现的。当通过BackingStore的接口更新数据模型时,它自动计算出旧版本和新版本之间的差异,差异包括插入的分区、插入的索引路径、删除的分区、删除的索引路径和移动的索引路径。然后自动使用此差异在UITableViewUICollectionView实例中进行平滑、高效的批量更新。这意味着您再也不需要调用reloadData(),对表格或收集视图内容的任何更改都将完美动画化。

很酷吧?

示例

要运行示例项目,首先克隆仓库,然后从示例目录中运行pod install。示例项目演示了如何使用BackingStore实例及其相关组件构建基本的表格视图。它从JSONPlaceholder(一个用于测试和原型的假在线REST API)异步加载数据。然后提供一些您可以执行的操作来更改表格视图的内容。这些更改可以触发相应的批量更新,表格视图的内容通过漂亮的动画更改。此README.md中使用的代码段来自示例项目。

安装

要使用CocoaPods进行安装,请将以下内容添加到您的项目Podfile中

pod 'BackingStore'

要使用Carthage进行安装,请将以下内容添加到您的项目Cartfile中

github "patricklynch/BackingStore"

优势

使用BackingStore将为性能、用户体验和开发者体验提供巨大的提升。由于表格视图和集合视图的内容不会被无谓地重新加载(通过调用redloadData()时的不受管制的调用),因此滚动和渲染性能得到改善。由于BackingStore采用批量更新动画,用户体验得到改善,因为它允许用户了解他们与显示数据的互动。这对需要加载、分页和显示错误等状态之间转换的应用程序来说非常好。如果没有BackingStore或类似的东西,想要表格视图或集合视图更新具有动画效果和性能的开发者将不得不手动计算和排队批量更新。在进行时,必须(并且很难)确保批量更新不会相互重叠。这不仅代码难以编写和维护,而且还常常是难以调试的烦人崩溃的原因。也许你之前已经遇到过这个错误

无效更新:第0部分项数无效。在更新后现有部分中包含的项数(1)必须等于更新前该部分中包含的项数(1),加或减从该部分插入或删除的项数(插入1项,未删除0项),以及加或减移入或移出该部分的项数(未移入0项,未移出0项)。

如果正确使用BackingStore,则不会出现此错误。如果你仍然看到这个错误,这通常意味着你存储在BackingStore实例中的某些数据类型不符合Hashable,或者它的Hashable符合性提供的hashValue不够唯一。关于这一点稍后会有更多介绍。

典型设置步骤

创建数据源

就像通常一样,这将为UICollectionViewDataSourceUITableViewDataSource提供实现。在这个例子中,我们将显示从以下url加载的待做事项https://jsonplaceholder.typicode.com/todos

class TodoDataSource: NSObject, UITableViewDataSource {

    // MARK: - UITableViewDataSource

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        fatalError("Nothing to do just yet.")
    }
}

创建一个 SectionType

BackingStore 是一个通用类,使用类型 SectionType 来唯一标识将在表格或集合视图中显示的每个区域。因此,您必须为此目的定义一个类型,该类型必须符合 HashableComparable 以满足泛型 SectionType 的约束。在本例中,我们将为我们的 toods 创建两个部分,分别表示已完成和未完成的任务。

enum SectionType: Int {
    case notCompleted, completed

    static func < (lhs: SectionType, rhs: SectionType) -> Bool {
        return lhs.rawValue < rhs.rawValue
    }
}

如果部分数量是动态的——也就是说,在编译时不固定——请使用一个具有关联值的枚举来提供对 Comparable 的符合性。这允许使用 group 的案例以及关联的 index 值识别许多部分。例如,如果有许多要显示的某种类型的“组”,可以创建如下所示的分区类型

enum MySectionType: Int {
    case group(index: Int)

    static func < (lhs: SectionType2, rhs: SectionType2) -> Bool {
        switch (lhs, rhs) {
        case (.group(let lhsIndex), .group(let rhsIndex)):
            return lhsIndex < rhsIndex
        }
    }
}

但是,如果您将只显示一个单独的区域,甚至不必为用作 BackingStore 泛型 SectionType 而创建类型。存在一个名为 SingleSectionType 的类型,它已经为此目的提供服务。 BackingStore 提供了扩展的 API,该 API 简化了使用 SingleSectionType 的实施的主要功能,这使得它更方便。以下展示了多区域 SectionTypeSingleSectionType 的使用。

创建一个 BackingStore 实例

现在您已经创建了一个 SectionType(或者如果您将使用 SingleSectionType),我们可以创建一个作为存储属性的 BackingStore 实例。您可以将该属性放在视图控制器、数据源或任何您喜欢的地方。正如我们很快将看到的,唯一重要的是每个组件都有对其他组件的正确引用。否则,您可以根据应用程序的需求自定义结构。

对于多个部分

let backingStore = BackingStore<MySectionType>()

对于单一部分部分

let backingStore = BackingStore<SingleSectionType>()

创建一个 BackingStoreDecorator

BackingStoreDecorator 是一个协议,它定义了一个对象,用于装饰表格视图或集合视图中的单元格。这是该框架的关键设计理念之一,即 解耦 dequeuingdecorating。在传统的 UITableViewDataSource 实现中,这两个任务在 tableView(_:cellForRowAt:) 函数中同时完成。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Dequeue
    let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell

    // Decorate
    let myData = myDataArray[indexPath.row]
    cell.title = myData.localizedTitle
    cell.backgroundColor = .white
    cell.addDropShadow()

    return cell
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return myDataArray.count
}

与以下实现相比,当数据源还符合 BackingStoreDataSource 时,需要实现 decorate(cell:at:) 函数

class MyDataSource: NSObject, UITableViewDataSource, BackingStoreDecorator {

    // MARK: - UITableViewDataSource

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Dequeue
        if backingStore.item(at: indexPath) is DescriptionData {
            cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)

        } else if backingStore.item(at: indexPath) is Action {
            cell = tableView.dequeueReusableCell(withIdentifier: "ActionCell", for: indexPath)
        } else {
            fatalError("Unsupported data type")
        }
        
        // Ask decorate to decorate newly-dequeued cell
        decorate(cell: cell, at: indexPath)
        return cell
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return backingStore.sectionCount
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return backingStore.section(at: section)?.itemCount ?? 0
    }

    // MARK: - BackingStoreDecorator

    func decorate(cell: UIView, at indexPath: IndexPath, animated: Bool) {
        if let cell = cell as? DescriptionCell,
            let data = backingStore.item(at: indexPath) as? DescriptionData {

            // Decorate
            cell.title = data.localizedText
            cell.backgroundColor = .white
            cell.addDropShadow()

        } else if let cell = cell as? ActionCell,
            let action = backingStore.item(at: indexPath) as? Action {

            // Decorate
            cell.title = action.localizedText
            cell.isEnabled = action.isEnabled
        }
    }
}

在这个示例中,dequeing仍然发生在tableView:cellForRow:atIndexPath:方法中,这里的代码仅决定创建哪种类型的单元格,然后将其传递给装饰器。实际的decorating则发生在装饰器实现的decorate(cell:at:)方法中,每个单元格的设置都是根据它将要表示的数据来确定的。将这两个阶段分开非常重要,这样就可以独立地进行。这种做法的好处是,在单元格可见时可以重新进行装饰,而无需像使用reloadData()重新加载时那样再次出列。

设置组件之间的引用

在一般情况下使用UICollectionViewUITableView时,必须设置dataSource属性为指定的UITableViewDataSourceUICollectionViewDataSource对象。当使用BackingStore时,需要建立两个连接:(1) 将数据源设置为表格视图的dataSource,(2) 将表格视图设置为数据源的backingStoreView

backingStore.view = tableView
backingStore.decorator = dataSource
tableView.dataSource = dataSource

BackingStoreView仅存在是为了使UICollectionViewUITableView能够扩展批处理更新的方法。这些更新和BackingStoreView API的输入与BackingStore的输出相匹配。在我们的配置中,我们创建的BackingStoreDataSource包含一个BackingStoreView实例和一个BackingStore实例,并将监管这些子组件之间的关系。

更新可见项目

BackingStore将不会排队进行任何批处理更新,直到它更新为要显示的数据。这是通过一个“update”函数完成的,您可以一次性提供所有要显示的内容。

class MyDataSource: NSObject, UITableViewDataSource, BackingStoreDataSource {

    let dataService = MyDataService()

    func loadData() {
        dataService.loadData() { [weak self] result in
            guard let result = result else { return }

            self?.backingStore.update(
                itemsForSections: [
                    .description: [result.description],
                    .actions: result.actions
                ],
                dataSource: self
            )
        }
    }
}

作者

Patrick Lynch: [email protected]

许可协议

BackingStore 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。