DataSourceController 1.2.0

DataSourceController 1.2.0

Pere Daniel PrietoPere Daniel Prieto 维护。



  • Pedro Daniel Prieto Martínez

DataSourceController 框架

Build Status Coverage Status CocoaPods compatible Carthage compatible Swift Package Manager compatible License Platform Language: Swift 4.2 Language: Swift 5.0

DataSourceController 是一个框架,提供了 UITableViewDataSourceUICollectionViewDataSource 协议的实现,并且通过 Section 实例提供了更声明性的 API。这使得我们能够从视图控制器中删除一些模板代码,从而使得代码库更加整洁。

需求

版本 需求
1.0.0 Xcode 10.0+
Swift 4.2+
iOS 10.0+

该框架使用 Swift 5.0 编写,但没有任何针对该 Swift 版本特定的代码。因此,它应该与使用 Swift 4.2 的项目兼容。

安装

您可以通过依赖管理器安装该框架。

使用 CocoaPods

要使用 CocoaPods,请首先确保您已安装它并按照 cocoapods.org 上的说明更新到最新版本。然后,您应该完成以下步骤:

  1. 将 DataSourceController 添加到您的 Podfile
pod 'DataSourceController', '~>1.2.0'
  1. 通过在命令行中执行以下命令,更新您的 pod 源并安装新 pod
$ pod install --repo-update

使用 Carthage

要使用 Carthage,首先请确保您已安装它并按照其仓库中的说明将其更新到最新版本。

  1. 将 DataSourceController 添加到您的 Cartfile
github "peredaniel/DataSourceController" ~> 1.2.0
  1. 运行 Carthage 以安装新框架
$ carthage update

使用 Swift Package Manager

使用 Swift Package Manager 安装时,请将以下内容添加到您的 Package.swift 文件中的 dependencies 部分

.package(url: "https://github.com/peredaniel/DataSourceController.git", .upToNextMinor(from: "1.2.0")),

开始使用

通过上述任何一种方法安装框架后,我们可以在任何 Swift 文件的 "header" 部分添加以下行以导入模块

import DataSourceController

第一步是实现我们的数据控制器。一个 "数据控制器" 是符合 CellDataController 协议的对象

protocol CellDataController {
    var reuseIdentifier: String { get }
    static func populate(with model: Any) -> CellDataController
}

数据控制器负责从模型对象获取数据(如果有必要),将其转换为可以由 UITableViewCellUICollectionViewCell 在屏幕上显示的数据结构。注意,数据控制器与一个标识符相关联,而不是与一个类相关联。特别是,这意味着您可以将同一个控制器用于具有相同重用标识符的不同细胞类。

数据控制器的简单示例如下

struct BasicCellDataController: CellDataController {
    let reuseIdentifier = "basicCell"
    let titleText: String

    static func populate(with model: Any) -> CellDataController {
    	guard let model = model as? String else {
    		return BasicCellDataController(titleText: "Incorrect data type")
    	}
        return BasicCellDataController(titleText: model)
    }
}

这是我们能够实现的最基本的控制器:它以 String 实例作为模型对象,仅仅在需要时存储它。当然,如果我们认为一个基本单元格只能显示标题的单元格,那么单元格的实际字体、对齐方式、背景或其他任何特定于 UI 的装饰器是单元格特定的,但所有这些背后的数据模型是相同的:一个 String。因此,使用相同的数据控制器是有意义的,这也说明了为什么将控制器与标识符相关联,而不是与类本身相关联。

接下来的一步是启用单元格从数据控制器检索数据。为此,我们必须使我们的单元格类符合 CellView 协议

public protocol CellView {
    func configure(with dataController: CellDataController)
}

按照上一个示例,我们可以扩展 UITableViewCell 以符合 CellView 协议,如下所示

extension UITableViewCell: CellView {
    public func configure(with dataController: CellDataController) {
    	guard let basicController = dataController as? BasicCellDataController else { return }
        textLabel?.text = basicController.titleText
    }
}

这将在数据控制器为 BasicCellDataController 时,设置一个 UITableViewCelltextLabel 属性的文本。请注意,如果我们有更多的数据控制器,这个函数需要区分它们。

现在我们有了数据控制器和准备操作的单元格,我们可以使用以下初始化方法之一创建一个 DataSourceController 的实例。

init(row: Any, delegate: DataSourceControllerDelegate?)

init(rows: [Any], delegate: DataSourceControllerDelegate?)

init(section: Section, delegate: DataSourceControllerDelegate?)

init(sections: [Section], delegate: DataSourceControllerDelegate?)

这些初始化器覆盖了你可能需要的所有可能情况。

  1. 用于列表的单个对象,没有头部或尾部。
  2. 要列表的对象包含在数组中,我们不需要头部或尾部。使用单个对象的数组与上一个情况等效。
  3. 我们需要一个单独的区段,具有头部、尾部或两者都有。我们也可以省略两者,从而回到上一个情况。
  4. 我们需要几个按给定数组顺序排列的区段,其中一些可以有一个或多个头部、尾部或两者都有。我们也可以省略所有的头部和尾部。

注意:代理是可选的,实际上可以省略。它也可以在创建后分配。我们将在后面描述它。

一旦我们有了实例 dataSourceControllerInstance,我们需要注册我们的数据控制器以使用一个特定的数据模型,如下所示。

dataSourceControllerInstance.register(dataController: BasicCellDataController.self, for: String.self)

最后,只需要将 dataSource 属性分配给我们的 UITableViewUICollectionView,如下所示。

myTableView.dataSource = dataSourceControllerInstance

...

myCollectionView.dataSource = dataSourceControllerInstance

下面是一个完整的简单例子

import DataSourceController
import UIKit

class TableViewController: UIViewController {
    @IBOutlet private var tableView: UITableView!

    private var rowEntries: [String] = ["First row", "Second row", "Third row", "Fourth row"]

    private lazy var dataSourceController: DataSourceController = {
    	let controller = DataSourceController(sections: rowEntries)
        controller.register(dataController: BasicCellDataController.self, for: String.self)
        return controller
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = dataSourceController
        tableView.reloadData()
    }
}

其中 BasicCellDataController 上面已有描述。

以上就是所有!当屏幕加载时,DataSourceController 将自动渲染表格视图。

区段模型

DataSourceController 的实现中,Section 是正在使用的主体模型对象。即使是接受一个 Any 数组的初始化器,也创建了一个 Section 的实例。一个 Section 简单地说是一个以下类的实例。

class Section {
    var rows: [Any]
    var sectionData: SectionDataModelType?
    init(sectionData: SectionDataModelType? = nil, rows: [Any])
}

注意,在初始化一个 Section 实例时,sectionData 参数是可选的,可以省略。

protocol SectionDataModelType {
    var header: String? { get }
    var footer: String? { get }
}

SectionDataModelType 是符合以下协议的任何对象

也就是说,带有特定区段头部和尾部的数据。

struct SectionDataModel: SectionDataModelType {
    private(set) var header: String?
    private(set) var footer: String?
    init(header: String? = nil, footer: String? = nil)
}

对于大多数用例,框架提供的默认协议实现已经足够了

DataSourceController 的初始化方法中传入的 delegate 参数是符合以下协议的任何对象

protocol DataSourceControllerDelegate: AnyObject {
    func backgroundMessageLabel(for view: UIView) -> UILabel?
    func backgroundEmptyView(for view: UIView) -> UIView?
    func dataSourceWasMutated(_: DataSourceController)
    func dataSourceWasMutated(_: DataSourceController, section: Int)
    func dataSourceWasMutated(_: DataSourceController, rows: [IndexPath])
}

所有函数都有一个默认的空实现,以使它们成为可选协议要求。

也就是说,代理主要通知由API(见下文API参考中详细信息)中可用的变异函数触发的DataSourceController实例的变化。通常情况下,代理会是拥有UITableViewUICollectionView实例的对象,因为很可能显示列表的一部分需要重新加载。

此外,如果DataSourceController实例的totalRowCount属性为0(表示没有行),控制器将尝试显示由该函数提供的UIView实例

func backgroundEmptyView(for view: UIView) -> UIView?

作为背景。请注意,此函数是可选的,因此可以省略。参数view是具有DataSourceController作为其dataSourceUITableViewUICollectionView实例。

注意:从版本1.2开始,协议声明中已删除backgroundMessageLabel(for:) -> UILabel?委托函数,无法使用。请使用backgroundEmptyView(for:) -> UIView?函数。

UITableView头部和尾部

UITableView实例中的节标题和尾部由框架使用符合协议的特定控制器自动管理

protocol SectionDataControllerType {
    var headerTitle: String? { get }
    var footerTitle: String? { get }
}

目前,框架确实使用了一个协议的内置实现

class SectionDataController: SectionDataControllerType {
    private(set) var headerTitle: String?
    private(set) var footerTitle: String?

    init(header: String?, footer: String?)
    convenience init(_ model: SectionDataModelType)
}

尽管SectionDataController是一个公开类,因此可以在框架外进行子类化,但DataSourceController的当前实现不允许使用任何其他类来显示UITableView头部/尾部。目前正在开发一种提供此类程度定制的方法。

UICollectionView头部和尾部

UICollectionView实例的头部和尾部在任何应用程序中都必须以不同的方式实现。因此,框架将UICollectionView实例的头部和尾部视为常规单元格。因此,必须实现并注册一个符合SectionDataModelType协议的模型对象的数据控制器。请注意,在这种情况下,可以使用自定义的节数据模型。

示例应用程序包含了一个UICollectionView头部类的实现示例及其与DataSourceController一起的使用方式。

API参考

完整的API参考可以在此处找到 API Reference

示例应用

本存储库包含iOS示例应用。示例应用包含了UITableViewUICollectionView的实现示例。此外,为了进行比较,MenuViewController类在其UITableView内容显示上没有使用DataSourceController

贡献

此框架基于MVVMDataSource开发。与“父”框架的主要区别在于更简单的方法、移除MVVM设计模式(使其无架构)、CoreData部分(尽管可以使用它们)以及操作。这种简单方法旨在满足我们自己的项目的需求,并使其在MVVM设计模式之外使用,但我们一旦满足需求(添加适当的文档)就停止了。如果您有任何改进想法,我们将很高兴听到/阅读。只需打开一个新的问题来进一步讨论,或者打开一个带有您想法的pull request。

代码风格指南和格式化器

我们遵循Ray Wenderlich Swift Style Guide,除了空白区域部分:我们使用4个空格而不是2个进行缩进。

为了执行上述代码风格指南中的指南,我们使用SwiftFormat。一组规则已检查到本存储库中的文件.swiftformat。在推送任何代码之前,请按照上述存储库中的如何安装说明安装SwiftFormat,并在项目的根目录中执行以下命令

swiftformat . --config .swiftformat --swiftversion 5.0 --exclude Package.swift

这将重新格式化项目文件夹内的所有*.swift文件,以遵循指南,但除外Package.swift声明文件。

持续集成和部署

我们使用 Travis CI 作为我们的持续集成解决方案,在开放式拉取请求和合并到 master 分支时运行构建和测试。合并任何拉取请求到 master 分支前,测试必须通过。当推送新的标签时,Travis CI 还负责将库部署到 Cocoapods 的 Trunk 存储库。