反应性数据显示管理器。 引言

这是ReactiveDataDisplayManager(RDDM)库上一系列文章的第一部分。 在本文中,我将描述使用“常规”表时必须处理的常见问题,并给出RDDM的描述。




问题1. UITableViewDataSource


对于初学者来说,忘记了职责分配,重用和其他很酷的话。 让我们看一下表的常规工作:

class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... } 

我们将分析最常见的选项。 我们需要执行什么? 正确地,通常实现3个UITableViewDataSource方法:

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) 

目前,我们将不关注辅助方法( numberOfSection等) func tableView(tableView: UITableView, indexPath: IndexPath)考虑最有趣的方法func tableView(tableView: UITableView, indexPath: IndexPath)

假设我们要在表格中填写带有产品说明的单元格,那么我们的方法将如下所示:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { let anyCell = tableView.dequeueReusableCell(withIdentifier: ProductCell.self, for: indexPath) guard let cell = anyCell as? ProductCell else { return UITableViewCell() } cell.configure(for: self.products[indexPath.row]) return cell } 

太好了,并不难。 现在,假设我们有几种类型的单元格,例如三种:

  • 产品展示
  • 股份清单;
  • 广告。

为了简化示例,我们将单元格设为getCell方法:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { switch indexPath.row { case 0: guard let cell: PromoCell = self.getCell() else { return UITableViewCell() } cell.configure(self.promo) return cell case 1: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.ad) return cell default: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.products[indexPath.row - 2]) return cell } } 

莫名其妙的很多代码。 假设我们要组成设置屏幕。 那里会有什么?

  • 带有化身的细胞帽;
  • 一组具有“深度”过渡的单元格;
  • 带开关的单元(例如,启用/禁用通过PIN码输入);
  • 具有信息的单元格(例如,将在其上放置电话,电子邮件等的单元格);
  • 个人优惠。

此外,设置顺序。 一个很棒的方法将会...

现在,另一种情况-有一个输入表格。 在输入表单上,有一堆相同的单元格,每个单元格负责数据模型中的特定字段。 例如,进入电话的单元格负责电话等等。
一切都很简单,但是只有一个“ BUT”。 在这种情况下,您仍然必须绘制不同的情况,因为您需要更新必要的字段。

您可以继续幻想并想象后端驱动设计,在该设计中,我们收到6种不同类型的输入字段,并且根据字段的状态(可见性,输入类型,验证,默认值等),单元的变化是如此之大,以至于它们不能导致单个界面。 在这种情况下,此方法将看起来非常不愉快。 即使您将配置分解为不同的方法。

顺便说一句,在那之后,想象一下您想要在工作时添加/删除单元格的代码是什么样的。 由于我们将被迫独立监视ViewController存储的数据和单元数的一致性,因此它看起来不会很好。

问题:

  • 如果存在不同类型的单元格,则代码将变成面条状;
  • 处理单元事件有很多问题。
  • 如果您需要更改表的状态,请使用丑陋的代码。

问题2。


尚需时机尚未成熟。
让我们看一下应用程序是如何工作的,或者更确切地说,数据是如何在屏幕上显示的。 我们总是按顺序介绍此过程。 好吧,或多或少:

  1. 从网络获取数据;
  2. 加工
  3. 在屏幕上显示此数据。

但是真的是这样吗? 不行 实际上,我们这样做:

  1. 从网络获取数据;
  2. 加工
  3. 保存在ViewController模型中;
  4. 某些原因导致屏幕刷新;
  5. 保存的模型将转换为单元格;
  6. 数据显示在屏幕上。

除了数量外,还存在差异。 首先,我们不再输出数据;而是将其输出。 其次,在数据处理过程中存在逻辑鸿沟,模型被保存并且过程结束。 然后发生了一些事情,另一个过程开始了。 因此,我们显然不会在屏幕上添加元素,而只是按需保存它们(顺便说一句,这也很麻烦)。

记住关于UITableViewDelegate ,它还包括确定单元格高度的方法。 通常, automaticDimension足够的,但有时这还不够,您需要自己设置高度(例如,对于动画或标题)
然后,我们通常会共享像元设置,具有高度配置的零件在另一种方法中。

问题:

  • 数据处理与其在UI上的显示之间的显式连接丢失;
  • 单元配置分为不同的部分。

主意


复杂屏幕上列出的问题会令人头疼,并且渴望喝茶。

首先,我不想不断实现委托方法。 显而易见的解决方案是创建一个将实现它的对象。 接下来,我们将执行以下操作:

 let displayManager = DisplayManager(self.tableView) 

太好了 现在,您需要该对象能够使用任何单元,而这些单元的配置需要移动到其他位置。

如果我们将配置放在一个单独的对象中,那么我们会将配置封装到一个地方(这是明智的做法)。 在同一个地方,我们可以采用格式化数据的逻辑(例如,更改日期格式,字符串串联等)。 通过相同的对象,我们可以订阅单元中的事件。

在这种情况下,我们将有一个具有两个不同接口的对象:

  1. UITableView实例生成接口用于我们的DisplayManager。
  2. 初始化,订阅和配置界面-用于Presenter或ViewController。

我们将此对象称为生成器。 然后,我们的表格生成器是一个单元格,以及其他所有单元格–一种在UI上呈现数据和处理事件的方式。

而且由于配置现在由生成器封装,并且生成器本身是一个单元,所以我们可以解决很多问题。 包括上面列出的那些。

实作


 public protocol TableCellGenerator: class { var identifier: UITableViewCell.Type { get } var cellHeight: CGFloat { get } var estimatedCellHeight: CGFloat? { get } func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell func registerCell(in tableView: UITableView) } public protocol ViewBuilder { associatedtype ViewType: UIView func build(view: ViewType) } 

通过这样的实现,我们可以进行默认实现:

 public extension TableCellGenerator where Self: ViewBuilder { func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier.nameOfClass, for: indexPath) as? Self.ViewType else { return UITableViewCell() } self.build(view: cell) return cell as? UITableViewCell ?? UITableViewCell() } func registerCell(in tableView: UITableView) { tableView.registerNib(self.identifier) } }<source lang="swift"> 

我将举一个小型发电机的示例:

 final class FamilyCellGenerator { private var cell: FamilyCell? private var family: Family? var didTapPerson: ((Person) -> Void)? func show(family: Family) { self.family = family cell?.fill(with: family) } func showLoading() { self.family = nil cell?.showLoading() } } extension FamilyCellGenerator: TableCellGenerator { var identifier: UITableViewCell.Type { return FamilyCell.self } } extension FamilyCellGenerator: ViewBuilder { func build(view: FamilyCell) { self.cell = view view.selectionStyle = .none view.didTapPerson = { [weak self] person in self?.didTapPerson?(person) } if let family = self.family { view.fill(with: family) } else { view.showLoading() } } } 

在这里,我们隐藏了配置和订阅。 注意,现在我们有了一个可以封装状态的地方(因为不可能将状态封装在单元格中,因为它被表重用了)。 他们也有机会“即时”更改单元中的数据。

注意self.cell = view 。 我们记得该单元,现在我们可以更新数据而无需重新加载该单元。 这是一个有用的功能。

但是我分心了。 由于我们可以使用生成器表示任何单元格,因此可以使DisplayManager的界面更加美观。

 public protocol DataDisplayManager: class { associatedtype CollectionType associatedtype CellGeneratorType associatedtype HeaderGeneratorType init(collection: CollectionType) func forceRefill() func addSectionHeaderGenerator(_ generator: HeaderGeneratorType) func addCellGenerator(_ generator: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType], after: CellGeneratorType) func addCellGenerator(_ generator: CellGeneratorType, after: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType]) func update(generators: [CellGeneratorType]) func clearHeaderGenerators() func clearCellGenerators() } 

实际上这还不是全部。 我们可以将生成器插入正确的位置或删除它们。

顺便说一句,在特定单元格之后插入一个单元格是非常有用的。 特别是如果我们逐渐加载数据(例如,用户输入了TIN,我们将上传TIN信息并通过在TIN字段之后添加几个新的单元格来显示)。

总结


现在,单元格的工作方式如下:

 class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } } 

或在这里:

 class ViewController: UIViewController { func update(fields: [Field]) { let gens = fields.map { field switch field.type { case .phone: let gen = PhoneCellGenerator(item) gen.didUpdate = { self.updatePhone($0) } return gen case .date: let gen = DateInputCellGenerator(item) gen.didTap = { self.showPicker() } return gen case .dropdown: let gen = DropdownCellGenerator(item) gen.didTap = { self.showDropdown(item) } return gen } } let splitter = SplitterGenerator() self.ddm.addGenerator(splitter) self.ddm.addGenerators(gens) self.ddm.addGenerator(splitter) } } 

我们可以控制元素的添加顺序,与此同时,数据处理与将元素添加到UI之间的连接也不会丢失。 因此,在简单的情况下,我们有简单的代码。 在困难的情况下,代码不会变成面食,同时看起来可以通过。 一个用于表的声明式界面已经出现,现在我们封装了单元格的配置,它本身使我们能够在不同屏幕之间重用单元格以及配置。

使用RDDM的优点:

  • 封装单元配置;
  • 通过将集合中的工作封装到适配器中来减少代码重复;
  • 选择一个适配器对象,其中包含处理集合的特定逻辑;
  • 代码变得更明显,更易于阅读;
  • 减少添加表所需编写的代码量;
  • 简化了处理单元事件的过程。

来源在这里

感谢您的关注!

Source: https://habr.com/ru/post/zh-CN466147/


All Articles