Descomponiendo una UICollectionViewCell

Después de ver Keynote WWDC 2019 y conocer SwiftUI , que está diseñado para describir declarativamente las IU en el código, quiero especular sobre cómo llenar declarativamente tabletas y colecciones. Por ejemplo, así:


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) 

En la colección, esto se representa de la siguiente manera:

imagen


Introduccion


Como saben por fuentes autorizadas : la gran mayoría de su tiempo, un desarrollador típico de iOS pasa trabajando con tabletas. Si asumimos que los desarrolladores en el proyecto carecen mucho y las tabletas no son simples, entonces no queda absolutamente ningún tiempo para el resto de la aplicación. Y hay que hacer algo con esto ... Una posible solución sería la descomposición celular.


La descomposición celular significa reemplazar una celda con varias celdas más pequeñas. Con este reemplazo, visualmente nada debería cambiar. Como ejemplo, considere las publicaciones de la fuente de noticias VK para iOS. Una publicación se puede presentar como una sola celda o como un grupo de celdas: primitivas .


Las células en descomposición no siempre funcionarán. Será difícil dividir una célula en primitivos que tengan una sombra o redondeo en todos los lados. En este caso, la celda original será una primitiva.


Pros y contras


Usando la descomposición de las celdas, las tablas / colecciones comienzan a consistir en primitivas que a menudo se reutilizarán: una primitiva con texto, una primitiva con una imagen, una primitiva con un fondo, etc. El cálculo de la altura de una única primitiva es mucho más simple y más efectivo que una celda compleja con una gran cantidad de estados. Si lo desea, la altura dinámica de la primitiva se puede calcular o incluso dibujar en el fondo (por ejemplo, texto a través de CTFramesetter ).


Por otro lado, trabajar con datos se vuelve más complicado. Se requerirán datos para cada primitiva, y de acuerdo con el IndexPath primitiva será difícil determinar a qué celda real pertenece. Tendremos que introducir nuevas capas de abstracción o resolver de alguna manera estos problemas.


Puede hablar durante mucho tiempo sobre los posibles pros y contras de esta empresa, pero es mejor tratar de describir el enfoque de la descomposición celular.


Elegir herramientas


Como UITableView capacidades limitadas y, como ya se mencionó, tenemos placas bastante complejas, una solución adecuada sería usar UICollectionView . Se trata de UICollectionView que se discutirá en esta publicación.


Al usar UICollectionView enfrenta a una situación en la que UICollectionViewFlowLayout base no puede formar la disposición deseada de los elementos de la colección (no tenemos en cuenta el nuevo UICollectionViewCompositionalLayout ). En esos momentos, generalmente se toma la decisión de encontrar alguna UICollectionViewLayout código UICollectionViewLayout . Pero incluso entre las soluciones preparadas, puede que no haya una adecuada, como, por ejemplo, en el caso de la página principal dinámica de una gran tienda en línea o red social. Asumimos lo peor, por lo que crearemos nuestra propia UICollectionViewLayout universal.


Además de la dificultad de elegir un diseño, debe decidir cómo recibirá los datos la colección. Además del enfoque habitual, donde un objeto (a menudo un UIViewController ) cumple con el protocolo UICollectionViewDataSource y proporciona datos para una colección, el uso de marcos basados ​​en datos está ganando popularidad. Los representantes brillantes de este enfoque son CollectionKit , IGListKit , RxDataSources y otros. El uso de tales marcos simplifica el trabajo con colecciones y proporciona la capacidad de animar cambios de datos, porque El algoritmo diferente ya está presente en el marco. Para fines de publicación, se seleccionará el marco RxDataSources .


Widget y sus propiedades


Introducimos una estructura de datos intermedia y la llamamos widget . Describimos las principales propiedades que debe tener un widget:


  1. El widget debe cumplir con los protocolos necesarios para usar el marco basado en datos. Tales protocolos suelen contener un valor asociado (por ejemplo, IdentifiableType en RxDataSources )
  2. Debería ser posible ensamblar widgets para diferentes primitivas en una matriz. Para lograr esto, el widget no debe tener valores asociados. Para estos fines, puede utilizar el mecanismo de borrado de tipo o algo así.
  3. El widget debería poder contar el tamaño de la primitiva. Luego, al formar el UICollectionViewLayout , solo queda colocar correctamente las primitivas de acuerdo con reglas predefinidas.
  4. El widget debe ser la fábrica para UICollectionViewCell . Por lo tanto, a partir de la implementación de UICollectionViewDataSource toda la lógica para crear celdas y todo lo que queda es:
     let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell 

Implementación de widgets


Para poder utilizar el widget con el marco RxDataSources , debe cumplir con los protocolos Equatable e IdentifiableType . Como el widget representa una primitiva, será suficiente para fines de publicación si el widget se identifica para cumplir con el protocolo IdentifiableType . En la práctica, esto afectará el hecho de que cuando el widget cambia, la primitiva no se volverá a cargar, sino que se eliminará e insertará. Para hacer esto, presente el nuevo protocolo WidgetIdentifiable :


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

Para cumplir con el WidgetIdentifiable , el widget debe cumplir con el protocolo Hashable . Hashable widget Hashable tomará datos para el cumplimiento del protocolo del objeto que describirá una primitiva particular. Puede usar AnyHashable para "borrar" el widget de tipo de objeto.


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

En esta etapa, se ejecutan las dos primeras propiedades del widget. Esto no es difícil de verificar mediante la recopilación de varios widgets con diferentes tipos de objetos en una matriz.


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

Para implementar las propiedades restantes, presentamos un nuevo protocolo WidgetPresentable


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

La función widgetSize(containerWidth:) se usará en UICollectionViewLayout al generar los atributos de las celdas, y widgetCell(collectionView:indexPath:) - para obtener las celdas.


Si el widget se WidgetPresentable protocolo WidgetPresentable , el widget cumplirá con todas las propiedades indicadas al comienzo de la publicación. Sin embargo, el objeto contenido en el widget AnyHashable tendrá que ser reemplazado por la composición WidgetPresentable y WidgetHashable , donde WidgetHashable no tendrá un valor asociado (como en el caso de Hashable ) y el tipo de objeto dentro del widget permanecerá "borrado":


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

En la versión final, el widget se verá así:


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

Objeto primitivo


Intentemos ensamblar la primitiva más simple, que será la sangría de una altura dada.


 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> es solo una subclase de UICollectionViewCell que acepta una UIView y la agrega como una subvista. cellDequeueSafely(indexPath:) es una función que registra una celda en una colección antes de reutilizarla si una celda de la colección no se ha registrado previamente. Spacing se utilizará como se describe al comienzo de la publicación.


Después de recibir la matriz de widgets, solo queda enlazar a 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 }) } } 

Resultados


En conclusión, me gustaría mostrar el trabajo real de la colección, que está construida completamente en widgets.


imagen

Como puede ver, la descomposición de UICollectionViewCell factible y en situaciones adecuadas puede simplificar la vida del desarrollador.


Observaciones


El código proporcionado en la publicación está muy simplificado y no debe compilarse. El objetivo era describir el enfoque, no proporcionar una solución llave en mano.


El protocolo WidgetPresentable se puede ampliar con otras funciones que optimizan el diseño, por ejemplo, widgetSizeEstimated(containerWidth:) o widgetSizePredefined(containerWidth:) , que devuelven el tamaño estimado y fijo, respectivamente. Vale la pena señalar que la función widgetSize(containerWidth:) debería devolver el tamaño de la primitiva incluso para cálculos exigentes, por ejemplo, para systemLayoutSizeFitting(_:) . Dichos cálculos se pueden almacenar en caché a través de Dictionary , NSCache , etc.


Como sabe, todos los tipos de celdas utilizados por UICollectionView deben registrarse previamente en la colección. Sin embargo, para reutilizar widgets entre diferentes pantallas / colecciones y no registrar todos los identificadores / tipos de celda por adelantado, debe adquirir un mecanismo que registre una celda inmediatamente antes de su primer uso dentro de cada colección. En la publicación, cellDequeueSafely(indexPath:) utilizó la función cellDequeueSafely(indexPath:) para esto.


No puede haber encabezados o pies de página en la colección. En su lugar habrá primitivas. La presencia de suplementarios en la colección no dará ninguna bonificación especial en el enfoque actual. Por lo general, se usan cuando la matriz de datos corresponde estrictamente al número de celdas y se requiere que muestre vistas adicionales antes, después o entre celdas. En nuestro caso, los datos para vistas auxiliares también se pueden agregar a la matriz de widgets y dibujar como primitivos.


Dentro de la misma colección, se pueden ubicar widgets con los mismos objetos. Por ejemplo, el mismo Spacing al principio y al final de la colección. La presencia de tales objetos no únicos conducirá al hecho de que la animación en la colección desaparece. Para hacer que tales objetos sean únicos, puede usar etiquetas especiales AnyHashable , #file y #line donde se creó el objeto, etc.

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


All Articles