一次,我(嗯,甚至不是我)都面临着将具有完全不同类型的一个单元格添加到具有特定类型单元格的
UICollectionView
的任务,并且仅在特殊情况下才执行此操作,该特殊情况是在“上方”处理的,并且不直接依赖于
UICollectionView
。 如果我的记忆力得到满足,此任务就会使
UICollectionViewDataSource
和
UICollectionViewDelegate
的
UICollectionViewDataSource
else
块变得很丑陋,这些
UICollectionViewDelegate
安全地放置在“生产”代码中,并且很可能不会在那儿走了。
在上述任务的框架内,没有任何想法考虑任何更优雅的解决方案,或者在此上浪费“周到”的精力。 但是,我记得这个故事:我考虑过尝试实现某个“数据源”对象,该对象可以由任意数量的其他“数据源”对象组成一个整体。 显然,该解决方案应归纳为适用于任意数量的组件(包括零个和一个),并且不依赖于特定的类型。 事实证明,这不仅是真实的,而且也不是太困难(尽管使代码“美丽”也要困难一些)。
我将展示如何处理
UITableView
示例。 如果您愿意,为
UICollectionView
编写类似的代码应该不会很困难。
“一个主意永远比其体现更重要”
这种格言属于伟大的漫画书作家艾伦·摩尔 ( “ Keepers ”,“ V代表仇杀队” , “杰出绅士联盟” ),但它实际上与编程无关,对吧?我的方法的主要思想是存储一个
UITableViewDataSource
对象的数组,返回其节的总数,并能够确定在访问该节时,原始“数据源”对象中的哪个将重定向此调用。
UITableViewDataSource
协议已经具有获取节,行等的数量的必要方法,但是不幸的是,在这种情况下,我发现使用它非常不方便,因为需要将对
UITableView
的特定实例的引用作为参数之一传递给它。 因此,我决定用几个其他简单成员扩展标准的
UITableViewDataSource
协议:
protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int }
事实证明,复合“数据源”是一个简单的类,可以实现
UITableViewDataSource
的要求,并且仅使用一个参数(一组
ComposableTableViewDataSource
的特定实例)进行初始化:
final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("\(#file) \(#line): Initializer with parameters must be used.") } }
现在仅需编写
UITableViewDataSource
协议的所有方法的实现,以便它们引用相应组件的方法。
“这是正确的决定。 我的决定
这些词属于俄罗斯联邦第一任总统 鲍里斯·尼古拉耶维奇·叶利钦 ( Boris Nikolayevich Yeltsin) ,并不真正引用以下文本,我只是喜欢它们。在我看来,使用
Swift语言的
功能是正确的决定,事实证明这很方便。
首先,我们实现一种返回节数的方法-这并不困难。 如上所述,我们只需要组件所有部分的总数即可:
func numberOfSections(in tableView: UITableView) -> Int {
(我不会解释标准函数的语法和含义。如果需要,互联网上到处都有
关于该主题的
不错的 入门 文章 。我也可以推荐一本
不错的书 。)
快速浏览
UITableViewDataSource
所有方法,您会注意到它们作为参数仅接受到表的链接以及节号或相应的
IndexPath
行的值。 我们将编写一些有助于我们实现所有其他协议方法的帮助程序。
首先,可以将所有任务简化为
“通用”函数,该函数以对特定
ComposableTableViewDataSource
的引用以及节号或
IndexPath
的值作为参数。 为了方便起见,我们将
假名分配给这些函数
的类型。 另外,为了增加可读性,我建议声明节号的别名:
private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T
(我将在下面解释所选名称。)
其次,我们实现了一个简单的函数,该函数根据
ComposedTableViewDataSource
节号确定特定的
ComposableTableViewDataSource
和相应的节号:
private func decompose(section: SectionNumber) -> (dataSource: ComposableTableViewDataSource, decomposedSection: SectionNumber) { var section = section var dataSourceIndex = 0 for (index, dataSource) in dataSources.enumerated() { let diff = section - dataSource.numberOfSections dataSourceIndex = index if diff < 0 { break } else { section = diff } } return (dataSources[dataSourceIndex], section) }
也许,如果您花了比我更长的时间,那么该实现将变得更优雅,更简单。 例如,同事立即建议我在此函数中实现
二进制搜索 (以前,例如,在初始化期间,通过组成段数的索引-一个简单的
整数 数组 )。 甚至花一点时间来编译和存储节号的对应表,但是代替使用
时间复杂度为 O(n)或O(log n)的方法,您可以以O(1)为代价获得结果。 但是我决定接受伟大的
Donald Knuth的建议,不要在没有明显需要和适当测量的情况下进行过早的优化。 是的,与本文无关。
最后,接受上面指示的
AdducedSectionTask
和
AdducedIndexPathTask
函数并将它们“重定向”到特定的
ComposedTableViewDataSource
实例:
private func adduce<T>(_ section: SectionNumber, _ task: AdducedSectionTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: section) return task(dataSource, decomposedSection) } private func adduce<T>(_ indexPath: IndexPath, _ task: AdducedIndexPathTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: indexPath.section) return task(dataSource, IndexPath(row: indexPath.row, section: decomposedSection)) }
现在,您可以解释我为所有这些功能选择的名称。 很简单:它们反映了一种实用的命名风格。 即 几乎没有什么意思,但听起来令人印象深刻。
最后两个函数看起来几乎像是双胞胎,但经过一番思考后,我放弃了摆脱代码重复的尝试,因为它带来了更多的不便,而不是优点:我必须将转换函数输出或转移到节号并返回到原始类型。 另外,重用这种通用方法的可能性趋于零。
实际上,所有这些准备工作和帮助程序都为协议方法的实现提供了难以置信的优势。 表配置方法:
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForHeaderInSection: $1) } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForFooterInSection: $1) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return adduce(section) { $0.tableView(tableView, numberOfRowsInSection: $1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return adduce(indexPath) { $0.tableView(tableView, cellForRowAt: $1) } }
插入和删除行:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { return adduce(indexPath) { $0.tableView?(tableView, commit: editingStyle, forRowAt: $1) } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
以类似的方式,可以实现对节索引头的支持。 在这种情况下,必须使用标题索引来代替节号。 同样,很可能为此添加一个附加字段到
ComposableTableViewDataSource
协议将很有用。 我将此片留在材料之外。
“今天不可能的明天将是可能的”
这些是理论航天学的创始人俄国科学家康斯坦丁·爱德华维奇·齐奥尔科夫斯基 ( Konstantin Eduardovich Tsiolkovsky )的话。首先,提出的解决方案不支持拖放行。 最初的计划包括对组成“数据源”对象之一中的拖放的支持,但是不幸的是,仅使用
UITableViewDataSource
不能实现此目的。 该协议的方法确定是否可以“拖放”特定行并在拖动结束时接收“回调”。
UITableViewDelegate
方法中隐含了事件本身的处理。
其次,更重要的是,有必要考虑一下更新屏幕上数据的机制。 我认为可以通过声明
ComposableTableViewDataSource
委托的协议来实现,该协议的方法将由
ComposedTableViewDataSource
实现并接收到原始“数据源”已收到更新的信号。 仍有两个问题悬而未决:如何可靠地确定在
ComposedTableViewDataSource
内部更改了哪个
ComposableTableViewDataSource
,以及它如何是一项单独的任务,而不是最琐碎的任务,但是有许多解决方案(例如,
诸如此类 )。 并且,当然,您将需要
ComposedTableViewDataSource
委托
ComposedTableViewDataSource
,该
ComposedTableViewDataSource
的方法将在更新复合“数据源”时调用,并由客户端类型(例如,
控制器或
视图模型 )实现。
我希望可以随着时间的推移更好地调查这些问题,并在本文的第二部分中介绍它们。 同时,我很高兴,您很好奇这些实验的信息!
聚苯乙烯
就在前几天,我不得不进入引言中提到的代码对其进行修改:我需要交换这两种类型的单元格。 简而言之,我不得不折磨自己,使
Index out of bounds
不断在不同的地方“谋生”。 使用所描述的方法时,只需交换作为初始化参数传递的数组中的两个“数据源”对象。
参考文献:-
带有完整代码和示例的Playgroud-
我的推特