分解UICollectionViewCell

在观看了Keynote WWDC 2019并了解了SwiftUI之后 ,我打算推测如何以声明性的方式填写盘子和收藏品, SwiftUI旨在用代码声明性地描述UI。 例如,像这样:


enum Builder { static func widgets(objects: Objects) -> [Widget] { let header = [ Spacing(height: 25).widget, Header(string: " ").widget, Spacing(height: 10, separator: .bottom).widget ] let body = objects .flatMap({ (object: Object) -> [Widgets] in return [ Title(object: object).widget, Spacing(height: 1, separator: .bottom).widget ] }) return header + body } } let objects: [Object] = ... Builder .widgets(objects: objects) .bind(to: collectionView) 

在集合中,呈现方式如下:

图片


引言


如您所知, 权威人士表示 :典型的iOS开发人员大部分时间都在使用平板电脑。 如果我们假设项目的开发人员非常缺乏,而平板电脑也不是那么简单,那么剩下的时间绝对没有剩下。 这需要做些事情。可能的解决方案是细胞分解。


细胞分解是指用几个较小的细胞代替一个细胞。 更换后,外观上什么都不会改变。 例如,请考虑来自VK新闻源(适用于iOS)的帖子。 一个帖子可以表示为单个单元格,也可以表示为一组单元格- 基本体


分解细胞并不总是有效。 很难将一个单元分解为在各个侧面都具有阴影或四舍五入的基元。 在这种情况下,原始单元将是原始的。


利弊


使用单元格的分解,表/集合开始由通常将被重用的基元组成:具有文本的基元,具有图片的基元,具有背景的基元等。 与具有大量状态的复杂单元格相比,单个基元高度的计算要简单得多,并且效率更高。 如果需要,可以计算图元的动态高度,甚至可以在背景中绘制它的高度(例如,通过CTFramesetter文本)。


另一方面,处理数据变得更加复杂。 每个原语都需要数据,并且根据原语的IndexPath将很难确定它属于哪个实际单元。 我们将不得不引入新的抽象层或以某种方式解决这些问题。


您可以长时间讨论这项计划的利弊,但最好尝试描述细胞分解的方法。


选择工具


由于UITableView有限,并且如上所述,我们的板块相当复杂,因此合适的解决方案是使用UICollectionView 。 关于UICollectionView ,将在本出版物中进行讨论。


使用UICollectionView遇到基本UICollectionViewFlowLayout无法形成集合元素的所需排列的情况(我们不考虑新的UICollectionViewCompositionalLayout )。 在这种情况下,通常会决定找到一些开源的UICollectionViewLayout 。 但是,即使在现成的解决方案中,也可能没有合适的解决方案,例如在大型在线商店或社交网络的动态主页的情况下。 我们假设最坏的情况,因此我们将创建自己的通用UICollectionViewLayout


除了选择布局的困难之外,您还需要确定集合将如何接收数据。 除了通常的方法(对象(通常是UIViewController )符合UICollectionViewDataSource协议并为集合提供数据)外,数据驱动框架的使用正变得越来越流行。 这种方法的杰出代表是CollectionKitIGListKitRxDataSources等。 使用此类框架简化了集合的工作,并提供了对数据更改进行动画处理的功能,因为 框架中已经存在差异算法。 出于发布目的,将选择RxDataSources框架。


小部件及其属性


我们介绍了一个中间数据结构,并将其称为小部件 。 我们描述了小部件应具有的主要属性:


  1. 小部件必须符合使用数据驱动框架的必要协议。 此类协议通常包含关联的值(例如RxDataSources中的IdentifiableType
  2. 应该可以将不同图元的窗口小部件组装成一个数组。 为此,小部件不应具有关联的值。 为此,您可以使用类型擦除机制或类似的机制。
  3. 小部件应该能够计算基元的大小。 然后,在形成UICollectionViewLayout ,仅保留根据预定义规则正确定位图元的时间。
  4. 小部件必须是UICollectionViewCell的工厂。 因此,从UICollectionViewDataSource的实现中UICollectionViewDataSource将删除所有用于创建单元格的逻辑,剩下的就是:
     let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell 

小部件实现


为了能够将窗口小部件与RxDataSources框架一起使用,它必须符合 EquatableIdentifiableType 协议 。 由于窗口小部件表示一个原语,因此如果窗口小部件标识自己符合IdentifiableType协议,就足以用于发布目的。 实际上,这将影响以下事实:当窗口小部件发生更改时,原语将不会重新加载,而是会删除并插入。 为此,请引入新的WidgetIdentifiable协议:


 protocol WidgetIdentifiable: IdentifiableType { } extension WidgetIdentifiable { var identity: Self { return self } } 

为了符合WidgetIdentifiable ,窗口小部件需要符合Hashable协议。 Hashable控件将从对象中获取数据以符合协议,该对象将描述特定的原语。 您可以使用AnyHashable来“擦除”对象类型窗口小部件。


 struct Widget: WidgetIdentifiable { let underlying: AnyHashable init(_ underlying: AnyHashable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.hash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying == rhs.underlying } } 

在此阶段,将执行小部件的前两个属性。 通过在数组中收集具有不同类型对象的几个小部件,这并不难检查。


 let widgets = [Widget("Hello world"), Widget(100500)] 

为了实现其余属性,我们引入了新协议WidgetPresentable


 protocol WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell func widgetSize(containerWidth: CGFloat) -> CGSize } 

生成单元格的属性时,将在UICollectionViewLayout使用widgetSize(containerWidth:)函数,并使用widgetCell(collectionView:indexPath:) -获取单元格。


如果小部件WidgetPresentable协议,则小部件将满足发布开始时指示的所有属性。 但是,必须将小部件AnyHashable中包含的对象替换为WidgetPresentableWidgetHashable ,其中WidgetHashable将不具有关联值(如Hashable的情况),并且小部件内部的对象类型将保持“擦除”:


 protocol WidgetHashable { func widgetEqual(_ any: Any) -> Bool func widgetHash(into hasher: inout Hasher) } 

在最终版本中,小部件将如下所示:


 struct Widget: WidgetIdentifiable { let underlying: WidgetHashable & WidgetPresentable init(_ underlying: WidgetHashable & WidgetPresentable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.widgetHash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying.widgetEqual(rhs.underlying) } } extension Widget: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return underlying.widgetCell(collectionView: collectionView, indexPath: indexPath) } func widgetSize(containerWidth: CGFloat) -> CGSize { return underlying.widgetSize(containerWidth: containerWidth) } } 

原始对象


让我们尝试组装最简单的图元,它是给定高度的缩进。


 struct Spacing: Hashable { let height: CGFloat } class SpacingView: UIView { lazy var constraint = self.heightAnchor.constraint(equalToConstant: 1) init() { super.init(frame: .zero) self.constraint.isActive = true } } extension Spacing: WidgetHashable { func widgetEqual(_ any: Any) -> Bool { if let spacing = any as? Spacing { return self == spacing } return false } func widgetHash(into hasher: inout Hasher) { self.hash(into: &hasher) } } extension Spacing: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cell: WidgetCell<SpacingView> = collectionView.cellDequeueSafely(indexPath: indexPath) if cell.view == nil { cell.view = SpacingView() } cell.view?.constraint.constant = height return cell } func widgetSize(containerWidth: CGFloat) -> CGSize { return CGSize(width: containerWidth, height: height) } } 

WidgetCell<T>只是UICollectionViewCell的子类,它接受UIView并将其添加为子视图。 cellDequeueSafely(indexPath:)是一个函数,如果该集合中的某个单元格先前未注册,则在重用之前在该集合中注册一个单元格。 Spacing将按照出版物开头所述的方式使用。


收到小部件数组后,它仅保留绑定到observerWidgets


 typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<WidgetSection> class Controller: UIViewController { private lazy var dataSource: DataSource = self.makeDataSource() var observerWidgets: (Observable<Widgets>) -> Disposable { return collectionView.rx.items(dataSource: dataSource) } func makeDataSource() -> DataSource { return DataSource(configureCell: { (_, collectionView: UICollectionView, indexPath: IndexPath, widget: Widget) in let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell }) } } 

结果


最后,我想展示该集合的真实工作,该集合完全基于小部件构建。


图片

如您所见, UICollectionViewCell的分解UICollectionViewCell可行的,并且在适当的情况下可以简化开发人员的工作。


备注


该出版物中给出的代码已非常简化,不应进行编译。 目的是描述方法,而不是提供统包解决方案。


WidgetPresentable协议可以使用其他优化布局的功能进行扩展,例如, widgetSizeEstimated(containerWidth:)widgetSizePredefined(containerWidth:) ,它们分别返回估计的大小和固定的大小。 值得注意的是,即使对于要求苛刻的计算,例如对于systemLayoutSizeFitting(_:)widgetSize(containerWidth:)函数也应返回原语的大小。 这样的计算可以通过DictionaryNSCache等进行缓存。


如您所知, UICollectionView使用的所有单元格类型UICollectionView必须在集合中预先注册。 但是,为了在不同的屏幕/集合之间重用小部件并且不预先注册所有单元格标识符/类型,您需要获得一种机制,该机制将在每个集合中首次使用单元格之前立即对其进行注册。 在出版物中, cellDequeueSafely(indexPath:)使用了函数cellDequeueSafely(indexPath:)


集合中不能有页眉或页脚。 原始元素将代替它们。 在目前的方法中,补充品的存在不会带来任何特别的好处。 通常,当数据数组严格对应于单元格数量并且需要在单元格之前,之后或之间显示其他视图时,才使用它们。 在我们的情况下,辅助视图的数据也可以添加到小部件数组中并绘制为原语。


在同一集合中,可以找到具有相同对象的小部件。 例如,在集合的开头和结尾使用相同的Spacing 。 这些非唯一对象的存在将导致以下事实:集合中的动画消失了。 要使此类对象唯一,可以使用特殊的AnyHashable标签,创建对象的#file#line等。

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


All Articles