📜 ⬆️ ⬇️

Composite "datasource" object and elements of the functional approach

Once I (well, not even me) faced the task of adding one cell of a completely different type to a UICollectionView with a certain cell type, and doing this only in a special case that is processed “above” and does not depend on UICollectionView directly. This task gave rise to, if my memory serves me, a couple of ugly if - else blocks inside the UICollectionViewDataSource and UICollectionViewDelegate , which safely settled into the “production” code and probably won't get anywhere from there.

Within the framework of the mentioned task, it makes no sense to think over any more elegant solution, to waste time and “thinking” energy, there was not. Nevertheless, I remember this story: I thought about trying to implement some kind of “datasource” object, which could be composed of any number of other “datasource” objects into a single whole. The solution, obviously, should be generalized, suitable for any number of components (including zero and one) and not depend on specific types. It turned out that this is not only real, but not too difficult (although it is a little harder to make the code also “beautiful”).

I will show what I got on the example of UITableView . If desired, to write a similar code for a UICollectionView should be no difficulty.
')

"The idea is always more important than its embodiment"


This aphorism belongs to the great comic book author Alan Moore ( “Keepers” , “V means Vendetta” , “League of Distinguished Gentlemen” ), but this is not exactly about programming, right?

The main idea of ​​my approach is to store an array of UITableViewDataSource objects, return their total number of sections, and be able to determine which of the original “datasource” objects to redirect this call to when accessing a section.

The UITableViewDataSource protocol already has the necessary methods for obtaining the number of sections, lines, etc., but, unfortunately, in this case I found it extremely inconvenient to use them because of the need to pass a link to a specific instance of UITableView as one of the arguments. So I decided to extend the standard UITableViewDataSource protocol with a couple of extra simple members:

 protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int } 

And the composite “datasource” turned out to be a simple class that implements the UITableViewDataSource requirements and is initialized with just one argument — a set of concrete instances of the ComposableTableViewDataSource :

 final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("ComposedTableViewDataSource: Initializer with parameters must be used.") } } 

Now it only remains to write the implementations of all methods of the UITableViewDataSource protocol so that they refer to the methods of the corresponding components.

“It was the right decision. My decision"


These words belonged to Boris Nikolayevich Yeltsin , the first president of the Russian Federation , and do not really relate to the text below, I just liked them.

The right decision seemed to me to use the functionality of the Swift language, and it really turned out to be convenient.

To begin with, we will implement a method that returns the number of sections - this is easy. As mentioned above, we only need the full number of all sections of the components:

 func numberOfSections(in tableView: UITableView) -> Int { // Default value if not implemented is "1". return dataSources.reduce(0) { $0 + ($1.numberOfSections?(in: tableView) ?? 1) } } 

(I will not explain the syntax and meaning of standard functions. If this is required, the Internet is full of good introductory articles on the topic . And I can also advise a pretty good book .)

Skimming through all the UITableViewDataSource methods, you notice that they take as arguments only the reference to the table and the value of either the section number or the corresponding IndexPath string. We will write several helpers who will be useful to us when implementing all the other protocol methods.

First, all tasks can be reduced to a “generic” function, which takes as its argument a link to the specific ComposableTableViewDataSource and the value of the section number or IndexPath . For convenience and brevity, we assign pseudonyms to the types of these functions. Plus, for additional readability, I propose to declare an alias for the section number:

 private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T 

(I will explain the chosen names below.)

Secondly, we will implement a simple function that, by the ComposedTableViewDataSource section ComposedTableViewDataSource determines the specific ComposableTableViewDataSource and the corresponding section number:

 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) } 

Perhaps, if you think a little longer than mine, the implementation will be more elegant and less straightforward. For example, colleagues immediately suggested that I implement a binary search in this function (previously, for example, during initialization, having compiled the index of the number of sections — a simple array of integers ). Or even spend a little time on the compilation and memory of storing the table of correspondence of the section numbers - but then instead of constantly using the method with the time complexity O (n) or O (log n) you can get the result with the price O (1). But I decided to use the advice of the great Donald Knuth not to engage in premature optimization without visible need and appropriate measurements. And not about this article.

And finally, the functions that take the AdducedSectionTask and AdducedIndexPathTask indicated above and “redirect” them to specific ComposedTableViewDataSource instances:

 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)) } 

And now you can explain the names I chose for all these functions. It's simple: they reflect a functional naming style. Those. little mean literally, but they sound impressive.

The last two functions look almost like twins, but, after thinking a little, I quit trying to get rid of code duplication because it brought more inconvenience than advantages: I had to output or transfer the conversion functions to the section number and back to the original type. In addition, the probability of reusing this generalized approach tends to zero.

All these preparations and helpers give an incredible advantage in implementing, in fact, the protocol methods. Table configuration methods:

 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) } } 

Insert and delete lines:

 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 { // Default if not implemented is "true". return adduce(indexPath) { $0.tableView?(tableView, canEditRowAt: $1) ?? true } } 

In a similar way, you can implement support for section index headers. In this case, instead of the section number, it is necessary to operate with a header index. It is also likely that it will be useful for this to add an additional field to the ComposableTableViewDataSource protocol. I left this part outside the material.

“The impossible today will be possible tomorrow”


These are the words of the Russian scientist Konstantin Eduardovich Tsiolkovsky , the founder of theoretical cosmonautics.

First, the solution presented does not support dragging lines. The original intention included dragging support within one of the components of the “datasource” objects, but, unfortunately, this cannot be done with just the UITableViewDataSource . The methods of this protocol determine whether a specific string can be “dragged” and receive a callback when the drag is completed. And the processing of the event itself is implied inside the UITableViewDelegate methods.

Secondly, more importantly, it is necessary to consider the mechanisms for updating the data on the screen. I think this can be done by declaring the ComposableTableViewDataSource delegate protocol, whose methods will be implemented by ComposedTableViewDataSource and receive a signal that the original “datasource” has received an update. Two questions remain open: how inside the ComposedTableViewDataSource reliably determine which ComposableTableViewDataSource has changed and how it is a separate and not the most trivial task, but having a number of solutions (for example, such ). And, of course, you will need the ComposedTableViewDataSource delegate ComposedTableViewDataSource , whose methods will be called when the composite “datasource” is updated and implemented by the client type (for example, a controller or a view-model ).

I hope over time to explore these issues better and cover them in the second part of the article. In the meantime, lol, you were curious to read about these experiments!

PS


Just the other day, I had to get into the code mentioned in the introduction to modify it: it took me to swap the cells of those two types. In short, I had to suffer and "beat" the Index out of bounds constantly appearing in different places. When using the described approach, it would only be necessary to swap two “datasource” objects in the array passed in as an initializer argument.

References:
- Playgroud with full code and example
- My Twitter

Source: https://habr.com/ru/post/442138/


All Articles