📜 ⬆️ ⬇️

Decomposing a UICollectionViewCell

After viewing Keynote WWDC 2019 and getting acquainted with SwiftUI , intended for the declarative description of the UI in the code, I would like to speculate on how you can declaratively fill tablets and collections. For example, like this:


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) 

In the collection, this is depicted as follows:

image


Introduction


As it is known from authoritative sources : the typical iOS developer spends the vast majority of his time at work with tablets. If we assume that the developers on the project are sorely lacking and the plates are not simple, then there is no time left for the rest of the application. And with this we need to do something ... A possible solution would be the decomposition of the cells.


Cell decomposition means the replacement of a single cell with several smaller cells. With this replacement, visually nothing should change. As an example, you can consider posts from the VK news feed for iOS. One post can be represented both in the form of a single cell, and in the form of a group of cells - primitives .


Decomposing the cells will not always work. It will be difficult to break into primitives a cell that has a shadow or rounding on all sides. In this case, the source cell will be a primitive.


Advantages and disadvantages


Using cell decomposition, tables / collections begin to consist of primitives that will often be reused: a primitive with text, a primitive with a picture, a primitive with a background, etc. The calculation of the height of a single primitive is much simpler and more efficient than a complex cell with a large number of states. If desired, the dynamic height of the primitive can be calculated or even drawn in the background (for example, text via CTFramesetter ).


On the other hand, work with data becomes more complicated. The data will be required for each primitive, and by the IndexPath primitive it will be difficult to determine to which real cell it belongs. We will have to introduce new layers of abstraction or somehow solve these problems.


It is possible to talk for a long time about possible pros and cons of this undertaking, but it is better to try to describe the approach to cell decomposition.


Choosing Tools


Since UITableView limited in its capabilities, and we, as already mentioned, have rather complicated tables, an adequate solution would be to use a UICollectionView . About UICollectionView also the speech in this publication will go.


Using a UICollectionView encounter a situation where the base UICollectionViewFlowLayout cannot form the required arrangement of collection elements (we do not take into account the new UICollectionViewCompositionalLayout ). At such times, it is usually decided to find some open-source UICollectionViewLayout . But even among the ready-made solutions may not be suitable, as, for example, in the case of the dynamic home page of a large online store or social network. We assume the worst, so we will create our own universal UICollectionViewLayout .


In addition to the difficulties with choosing a layout, it is necessary to decide how the collection will receive data. In addition to the usual approach, where an object (most often a UIViewController ) conforms to the UICollectionViewDataSource protocol and provides data for a collection, the use of data-driven frameworks is gaining popularity. Vivid representatives of this approach are CollectionKit , IGListKit , RxDataSources and others. Using such frameworks simplifies working with collections and provides the ability to animate data changes, since The diffing algorithm is already present in the framework. For publishing purposes, the RxDataSources framework will be selected.


Widget and its properties


We introduce an intermediate data structure and call it a widget . We describe the basic properties that the widget should have:


  1. The widget must comply with the necessary protocols to use the data-driven framework. Such protocols typically contain an associated value (for example, IdentifiableType in RxDataSources )
  2. It should be possible to collect widgets for different primitives in an array. To achieve this, the widget should not have associated values. For these purposes, you can use the type erase mechanism or something like that.
  3. The widget must be able to read the size of the primitive. Then, when forming the UICollectionViewLayout , it will only be UICollectionViewLayout to correctly position the primitives according to the rules provided in advance.
  4. The widget must be a factory for a UICollectionViewCell . Therefore, all the cell creation logic will be removed from the UICollectionViewDataSource implementation and only the following will remain:
     let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell 

Widget implementation


To be able to use the widget with the RxDataSources framework, it must comply with the Equatable and IdentifiableType protocols . Since the widget represents a primitive, it will be sufficient for publication purposes if the widget identifies itself to conform to the IdentifiableType protocol. In practice, this will affect the fact that when a widget changes, it will not be a reboot of the primitive, but deletion and insertion. To do this, we introduce a new protocol WidgetIdentifiable :


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

To match the WidgetIdentifiable , the widget must conform to the Hashable protocol. The data for compliance with the Hashable protocol will be taken from the object that will describe the particular primitive. You can use AnyHashable to "erase" an object type widget.


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

At this stage, the first two properties of the widget are executed. It is easy to check by gathering into the array several widgets with different types of objects.


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

To implement the remaining properties, we introduce the new WidgetPresentable protocol WidgetPresentable


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

The widgetSize(containerWidth:) function will be used in UICollectionViewLayout when forming cell attributes, and widgetCell(collectionView:indexPath:) will be used to get cells.


When the widget WidgetPresentable protocol, the widget will execute all the properties indicated at the beginning of the publication. However, the object contained within the AnyHashable widget will have to be replaced with the composition WidgetPresentable and WidgetHashable , where WidgetHashable will not have an associated value (as is the case with Hashable ) and the object type inside the widget will remain "erased":


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

In the final version, the widget will look like this:


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

Primitive object


Let's try to collect the simplest primitive, which will be an indent of a given height.


 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> is just a subclass of UICollectionViewCell that accepts a UIView and adds it as a subview. cellDequeueSafely(indexPath:) is a function that registers a cell in the collection before being reused if the cell has not previously been registered in the collection. Spacing will be used as described at the very beginning of the publication.


After receiving the array of widgets, it will only zabindit 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 }) } } 

results


In conclusion, I would like to show the real work of the collection, which is built entirely on widgets.


image

As you can see, the decomposition of UICollectionViewCell feasible and, in appropriate situations, can simplify the life of the developer.


Remarks


The code in the publication is very simplified and should not be collected. The goal was to describe the approach, rather than provide a ready-made solution.


The WidgetPresentable protocol can be extended with other functions allowing optimization of the layout, for example, widgetSizeEstimated(containerWidth:) or widgetSizePredefined(containerWidth:) , which return the estimated and fixed size, respectively. It is worth noting that the widgetSize(containerWidth:) function widgetSize(containerWidth:) must return the size of the primitive even for demanding calculations, for example, for systemLayoutSizeFitting(_:) . Such calculations can be cached through a Dictionary , NSCache , etc.


As you know, all cell types used by the UICollectionView must be registered in the collection beforehand. However, in order to re-use widgets between different screens / collections and not to register in advance all cell identifiers / types, you need to acquire a mechanism that will register the cell immediately before its first use within each collection. The publication used the cellDequeueSafely(indexPath:) function for this.


In the collection there can be no headers or footers. In their place will be primitives. The presence of the supplementary in the collection will not give any special bonuses in the current approach. Usually they are used when the data array strictly corresponds to the number of cells and it is required to show additional views before, after or between the cells. In our case, data for auxiliary views can also be added to the array of widgets and drawn as primitives.


Within the same collection can be widgets with the same objects. For example, the same Spacing at the beginning and at the end of the collection. The presence of such non-unique objects will lead to the fact that the animation in the collection will disappear. To make such objects unique, you can use special AnyHashable tags, #file and #line places to create an object, etc.


')

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


All Articles