在观看了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
协议并为集合提供数据)外,数据驱动框架的使用正变得越来越流行。 这种方法的杰出代表是CollectionKit , IGListKit , RxDataSources等。 使用此类框架简化了集合的工作,并提供了对数据更改进行动画处理的功能,因为 框架中已经存在差异算法。 出于发布目的,将选择RxDataSources框架。
小部件及其属性
我们介绍了一个中间数据结构,并将其称为小部件 。 我们描述了小部件应具有的主要属性:
- 小部件必须符合使用数据驱动框架的必要协议。 此类协议通常包含关联的值(例如RxDataSources中的IdentifiableType )
- 应该可以将不同图元的窗口小部件组装成一个数组。 为此,小部件不应具有关联的值。 为此,您可以使用类型擦除机制或类似的机制。
- 小部件应该能够计算基元的大小。 然后,在形成
UICollectionViewLayout
,仅保留根据预定义规则正确定位图元的时间。 - 小部件必须是
UICollectionViewCell
的工厂。 因此,从UICollectionViewDataSource
的实现中UICollectionViewDataSource
将删除所有用于创建单元格的逻辑,剩下的就是:
let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell
小部件实现
为了能够将窗口小部件与RxDataSources框架一起使用,它必须符合 Equatable
和IdentifiableType
协议 。 由于窗口小部件表示一个原语,因此如果窗口小部件标识自己符合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中包含的对象替换为WidgetPresentable
和WidgetHashable
,其中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:)
函数也应返回原语的大小。 这样的计算可以通过Dictionary
, NSCache
等进行缓存。
如您所知, UICollectionView
使用的所有单元格类型UICollectionView
必须在集合中预先注册。 但是,为了在不同的屏幕/集合之间重用小部件并且不预先注册所有单元格标识符/类型,您需要获得一种机制,该机制将在每个集合中首次使用单元格之前立即对其进行注册。 在出版物中, cellDequeueSafely(indexPath:)
使用了函数cellDequeueSafely(indexPath:)
。
集合中不能有页眉或页脚。 原始元素将代替它们。 在目前的方法中,补充品的存在不会带来任何特别的好处。 通常,当数据数组严格对应于单元格数量并且需要在单元格之前,之后或之间显示其他视图时,才使用它们。 在我们的情况下,辅助视图的数据也可以添加到小部件数组中并绘制为原语。
在同一集合中,可以找到具有相同对象的小部件。 例如,在集合的开头和结尾使用相同的Spacing
。 这些非唯一对象的存在将导致以下事实:集合中的动画消失了。 要使此类对象唯一,可以使用特殊的AnyHashable
标签,创建对象的#file
和#line
等。