Red de conocimiento informático - Material del sitio web - Cómo escribir un UITableView excelente

Cómo escribir un UITableView excelente

Si cree que muchos métodos en los protocolos UITableViewDelegate y UITableViewDataSource se copian y pegan cada vez, y los métodos de implementación son similares, si cree que iniciar solicitudes de red y analizar datos requiere una gran sección de código, y si se agregan actualizaciones y cargas; , entonces es simplemente demasiado complicado; si te preguntas por qué el siguiente código hace todo lo anterior, no sabes qué hacer. Si te preguntas por qué el siguiente código hace todo lo anterior:

MVC

¡Abróchate el cinturón y súbete al auto!

Antes de discutir el desacoplamiento, primero comprendamos el núcleo de MVC: el controlador (en adelante, C) es responsable de la interacción entre el modelo (en adelante, M) y la vista (en adelante, denominada como V).

Dicha M no suele ser una clase separada; en muchos casos, es una capa de clases. El nivel superior suele ser una clase que termina en Modelo, que está a cargo directamente de C.

La clase Modelo también puede contener dos objetos:

Item: Es el objeto que realmente almacena los datos. Puede entenderse como un diccionario, que se corresponde uno a uno con los atributos en V

Almacenamiento en caché: almacena en caché sus propios elementos (si hay muchos)

Malentendidos comunes:

Normalmente, el procesamiento de datos se coloca en M, no en C (C solo hace cosas que no se pueden reutilizar)

Desacoplar no se trata solo de sacar un fragmento de código de la caja. Más bien, se trata de poder fusionar código duplicado y tener una buena función de arrastrar y soltar.

Versión original

En C, creamos un objeto UITableView y configuramos su fuente de datos y los delegamos en nosotros mismos. Es decir, gestiona por sí solo la lógica de la interfaz de usuario y la lógica de acceso a los datos. En esta arquitectura, los principales problemas son:

En comparación con el patrón MVC, ahora V contiene C y M.

C gestiona toda la lógica y el grado de acoplamiento es demasiado alto.

La mayor parte de la interfaz de usuario la realiza Cell, no UITableView en sí.

Para resolver estos problemas, primero debemos descubrir qué hacen la fuente de datos y el proxy.

Fuente de datos

Tiene dos métodos proxy que deben implementarse:

- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)sección ;

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath;

En resumen, siempre que se implementen estos dos métodos, un simple El El objeto UITableView está completo.

Además, también se encarga de gestionar el número de secciones, títulos, edición y movimiento de celdas, etc.

Agente

El agente implica principalmente los siguientes aspectos:

Devoluciones de llamada antes y después de la visualización de celdas, vista de encabezado, etc.

Los eventos de altura y clic de las celdas, headerView, etc.

Los dos métodos más utilizados:

- (CGFloat)tableView: (UITableView *)tableView heightForRowAtIndexPath: (NSIndexPath *)indexPath

- (void; ) tableView: (UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath;

Recordatorio: la mayoría de los métodos proxy tienen un parámetro indexPath

Optimice la fuente de datos

La forma más sencilla es tener la fuente de datos como un objeto independiente.

Este método de escritura tiene un cierto efecto de desacoplamiento y reduce efectivamente la cantidad de código en lenguaje C.

Este método de escritura no solo reduce efectivamente la cantidad de código en lenguaje C, sino que también desempeña un cierto papel de desacoplamiento. Nuestro objetivo es reducir el código innecesario.

Por ejemplo, obtener el número de filas en cada sección siempre se implementa mediante una lógica muy similar. Sin embargo, dado que la implementación específica de las fuentes de datos no es uniforme, se debe volver a implementar para cada fuente de datos.

SectionObject

Primero, pensemos en una pregunta: cuando la fuente de datos M guarda un elemento, ¿cómo se ve? La respuesta es una matriz bidimensional donde cada elemento contiene toda la información requerida para una sección

. Entonces, además de tener su propio array (para las celdas), también están los títulos de los capítulos, etc.

A este tipo de elemento le llamamos SectionObject:

@interface KtTableViewSectionObject:

@property (nonatomic, copy) NSString *headerTitle // Puedes usar el método titleForHeaderInSection en el protocolo UITableDataSource

p>

@property (nonatomic, copy) NSString * footerTitle; // Puedes usar el método titleForFooterInSection en el protocolo UITableDataSource

@property (nonatomic, reservado) NSMutableArray *items;

- (instancetype)initWithItemArray: (NSMutableArray *)items;

@end

Item

La matriz Item debe almacenar los elementos necesarios para cada celda, teniendo en cuenta las características de la celda, la clase base de BaseItem se puede diseñar así:

@interface KtTableViewBaseItem:

@property (nonatomic, retener) NSString *itemIdentifier;

@property (no atómico, retener) UIImage *itemImage;

@property (no atómico, retener) NSString *itemTitle

@property (no atómico) , retener) NSString *itemSubtitle;

@property (nonatomic, retener) UIImage *itemAccessoryImage

- (instancetype)initWithImage: (UIImage *) título de la imagen: (NSString *)title Subtítulo: (NSString *)subTitle AccessoryImage (UIImage *)accessoryImage;

@end

Código de implementación principal

Después de especificar el formato de almacenamiento de datos unificado, podemos considere completar ciertas tareas en el método de la clase base.

Tome -

(NSInteger)tableView: (UITableView *)tableView

numberOfRowsInSection: (NSInteger)section método como ejemplo, se puede implementar de esta manera:

- ( NSInteger )tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)sección {

if (self.sections.count gt; sección) {

KtTableViewSectionObject *sectionObject = [ self.sections objectAtIndex :sección];

devolver secciónObject.items.count;

}

devolver 0;

}

La parte más difícil es crear la celda, porque no sabemos el tipo de celda, por lo que, naturalmente, no podemos llamar al método alloc. Más allá de eso, necesitamos crear celdas y configurar la interfaz de usuario, algo que una fuente de datos no debería hacer.

El método para resolver estos dos problemas es el siguiente:

Defina un protocolo, la clase principal devuelve la clase base Cell y la subclase devuelve el tipo correspondiente.

Agregue un método setObject a Cell para analizar el elemento y actualizar la interfaz de usuario.

Ventajas

Después del procesamiento anterior, las ventajas son muy obvias:

La fuente de datos de la subclase solo necesita implementar el método cellClassForObject. Los métodos de fuente de datos originales se han implementado de manera uniforme en la clase principal.

Cada celda solo necesita escribir su propio método setObject y luego esperar a que se cree y se llame.

Las subclases pueden obtener elementos rápidamente a través del método objectForRowAtIndexPath sin anularlos.

Controla la demostración para sentir el efecto (SHA-1: 6475496).

Optimización de servidores proxy

Tomemos como ejemplos los dos métodos comúnmente utilizados en los protocolos de proxy mencionados anteriormente y veamos cómo optimizarlos y desacoplarlos.

La primera es calcular la altura. Esta lógica no necesariamente se completa en lenguaje C porque involucra la interfaz de usuario, por lo que Cell es responsable de la implementación. La base para calcular la altura es el Objeto, por lo que agregamos un método de clase en la clase base Cell:

(CGFloat)tableView: (UITableView*)tableView rowHeightForObject: (KtTableViewBaseItem *) object

<; p>Otro tipo de problema está representado por el método proxy para manejar eventos de clic, cuya característica principal es el uso del parámetro indexPath para representar la ubicación. Sin embargo, en la práctica, no nos importa la ubicación, sino los datos de esa ubicación.

Por lo tanto, encapsulamos el método proxy para que C pueda llamar al método con parámetros de datos. Dado que este objeto de datos se puede obtener de la fuente de datos, debemos poder obtener el objeto de la fuente de datos en el método proxy.

La mejor manera de hacer esto es heredar de UITableView:

@protocol KtTableViewDelegate@optional

- (void)didSelectObject: (id) objeto enIndexPath: (NSIndexPath*)indexPath;

- (UIView *)headerViewForSectionObject: (KtTableViewSectionObject *)sectionObject atSection.(NSInteger)section;

// En el futuro, las unidades también pueden ser Celdas editar, intercambiar, deslizar hacia la izquierda y otras operaciones. Devolución de llamada

// Este protocolo hereda de UITableViewDelegate, así que realice la transferencia de un nivel usted mismo. VC aún necesita implementar algunas funciones

@end

@. interfaz KtBaseTableView :UITableView@property (nonatomic, asignar) id ktDataSource

@property (nonatomic, asignar) id ktDelegate;

@end

La implementación de la altura de la celda es como A continuación, llama al método de origen de datos para obtener datos:

- (CGFloat)tableView: (UITableView*)tableView heightForRowAtIndexPath: (NSIndexPath*) indexPath {

id dataSource = (id)tableView .dataSource;

KtTableViewBaseItem *object = [dataSource tableView: tableView objectForRowAtIndexPath: indexPath]

Class cls = [dataSource tableView: tableView cellClassForObject: objeto]; >return [cls tableView: tableView rowHeightForObject: object];

}

Ventajas

Al encapsular UITableViewDelegate (que en realidad se realiza principalmente a través de UITableView), obtenga las siguientes características:

C No tiene que preocuparse por UITableViewDelegate.p>

C No tiene que preocuparse por la altura de la celda, depende de cada clase de celda

Si los datos en sí existen en la fuente de datos, se pueden pasar a C a través del protocolo proxy, eliminando la necesidad de que C vuelva a visitar la fuente de datos.

Si los datos no existen en la fuente de datos, entonces los métodos del protocolo proxy se reenviarán normalmente (porque el protocolo proxy personalizado hereda de UITableViewDelegate)

Por presentación (SHA-1 : ca9b261 ) y siente los efectos.

Más MVC, más conciso

En los dos paquetes anteriores, en realidad cambiamos el UITableView que contiene el proxy local y la fuente de datos al que contiene el proxy personalizado y la fuente de datos. KtTableView. E implementa muchos métodos del sistema de forma predeterminada.

Hasta ahora, todo parece completo, pero en realidad hay algunas cosas que podrían mejorarse:

¡Aún no es un modelo MVC!

La lógica y la implementación de C aún se pueden simplificar

Basándonos en estas consideraciones, implementamos una subclase de UIViewController y encapsulamos la fuente de datos y el proxy en C.

@interface KtTableViewController: UIViewController@property (no atómico, fuerte) KtBaseTableView *tableView

@property (no atómico, fuerte) KtTableViewDataSource *dataSource; (no atómico, asignar) UITableViewStyle tableViewStyle // Se utiliza para crear tableView.

- (instancetype)initWithStyle: (UITableViewStyle)style;

@end

Para garantizar que las subclases creen fuentes de datos, definimos este método como un protocolo. y definir según sea necesario.

Resultados frente a objetivos

Ahora, determinemos qué hacer con el TableView mejorado:

Primero, debe crear un controlador de vista A que herede de KtTableViewController y llama a su método initWithStyle.

KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle: UITableViewStylePlain];

Implemente el método createDataSource en la subclase VC para implementar el enlace de fuente de datos.

- (void)createDataSource {

self.dataSource = [[KtMainTableViewDataSource alloc] init] // Este paso crea la fuente de datos

}

En la fuente de datos, debe especificar el tipo de celda.

- (Class)tableView: (UITableView *)tableView cellClassForObject: (KtTableViewBaseItem *)object {

return [clase KtMainTableViewCell]

}

En Cell, debes analizar los datos para actualizar la interfaz de usuario y devolver tu propia altura.

(CGFloat)tableView: (UITableView *)tableView rowHeightForObject: (KtTableViewBaseItem *)objeto {

return 60;

}

/

La demostración hereda el método setObject de la clase padre.

¿Qué más necesita optimización?

Hasta ahora, hemos implementado la encapsulación de UITableView y sus protocolos y métodos relacionados, lo que facilita su uso y evita una gran cantidad de código duplicado y sin sentido. .

Cuando usamos UITableView, necesitamos crear un controlador, una fuente de datos y una celda personalizada, que se basa en el patrón MVC. Por lo tanto, se puede decir que hemos hecho un trabajo bastante bueno en encapsulación y desacoplamiento, e incluso si nos esforzamos más, será difícil lograr mejoras significativas.

Sin embargo, la discusión sobre UITableView está lejos de terminar. He enumerado los siguientes problemas que deben resolverse

Bajo este diseño, para obtener retorno de datos (por ejemplo, la celda). devuelve C enviando un mensaje) no es fácil.

Cómo integrar la actualización desplegable y la carga desplegable

Cómo integrar el inicio de solicitudes de red y los datos de análisis

En cuanto a la primera pregunta, en De hecho, la interacción entre V y C en el patrón MVC común se puede mantener directamente agregando atributos débiles en Cell (u otras clases), o definiendo un protocolo.

Las preguntas 2 y 3 son otro gran tema. Todos pueden implementar solicitudes de red, pero cómo integrarlas elegantemente en el marco para garantizar la simplicidad y escalabilidad del código es una cuestión que merece una consideración en profundidad. preguntas de investigación. A continuación nos centraremos en las solicitudes de red.

Por qué crear una capa de red

¿Cómo debería diseñarse el marco de la capa de red de iOS

? Esta es una pregunta muy amplia y que no puedo responder. Existen algunas ideas y soluciones excelentes y probadas, por lo que decidí hablar sobre cómo diseñar una capa de red común y simple desde la perspectiva de un desarrollador normal en lugar de la de un arquitecto. Creo que incluso las arquitecturas más complejas evolucionan a partir de diseños simples.

Para la mayoría de las aplicaciones pequeñas, la integración de marcos de solicitud de red como AFNetworking es suficiente para satisfacer las necesidades de más del 99%. Sin embargo, a medida que se desarrolla el proyecto, o a largo plazo, llamar directamente a un marco de red específico en VC (tomando AFNetworking como ejemplo) provocará al menos los siguientes problemas:

Si AFNetworking detiene el mantenimiento en el futuro, y si es necesario reemplazar el marco de la red, el costo será inimaginable. Todos los VC tienen que realizar cambios en el código y la mayoría de los cambios son los mismos.

Hay algunos ejemplos de la vida real, como nuestro proyecto que todavía utiliza ASIHTTPRequest, obsoleto desde hace mucho tiempo, y es previsible que este marco sea reemplazado tarde o temprano.

Es posible que los marcos existentes no satisfagan nuestras necesidades. Por ejemplo, en ASIHTTPRequest, NSOperation

se utiliza internamente para representar cada solicitud de red. Como todos sabemos, cancelar una NSOperation no es simplemente llamar al método cancelar

. De hecho, una vez que lo pones en la cola, es imposible cancelarlo sin modificar el código fuente.

A veces, necesitamos hacer más que simplemente realizar una solicitud de red, necesitamos personalizar la solicitud de varias maneras. Por ejemplo, es posible que queramos calcular las horas de inicio y finalización de las solicitudes para calcular el tiempo dedicado a las solicitudes de red, el análisis de datos, etc.

A veces queremos diseñar un componente común y permitir que diferentes unidades de negocio personalicen reglas específicas. Por ejemplo, diferentes departamentos pueden agregar encabezados diferentes a las solicitudes HTTP

.

Es posible que las solicitudes web también deban agregar una gran cantidad de otros requisitos, como ventanas emergentes cuando la solicitud falla, registro de solicitudes, etc.

Consulte el código actual (SHA-1: a55ef42) para obtener una idea del diseño sin ninguna capa de red.

Cómo diseñar una capa de red

La solución es realmente muy sencilla:

Todos los problemas informáticos se pueden solucionar añadiendo una capa intermedia

Reader Puedes pensar por ti mismo por qué agregar una capa intermedia puede resolver los tres problemas anteriores.

Tres módulos principales

Para un marco web, creo que hay tres aspectos principales dignos de diseño:

Cómo solicitar

Cómo devolver la llamada

Análisis de datos

Una solicitud web completa generalmente consta de los tres módulos anteriores. Analicemos las precauciones al implementar cada módulo uno por uno:

Iniciar una solicitud<. /p >

Al iniciar una solicitud, generalmente hay dos ideas. La primera es escribir todos los parámetros que deben configurarse en el mismo método; consulte el artículo "Avanzando con los tiempos, diseño de arquitectura de capa de red de iOS". bajo HTTP/2" Código:

(void)networkTransferWithURLString: (NSString *)urlString

andParameters: (NSDictionary *)parameters

isPOST: (BOOL) isPost

transferType: (NETWORK_TRANSFER_ TYPE)transferType

andSuccessHandler: (void (^)(id ResponseObject))successHandler

andFailureHandler: (void (^)( NSError *error ) )failureHandler {

// Encapsulación AFN

}

La ventaja de este método de escritura es que todos los parámetros son claros de un vistazo y es fácil de usar. Sólo es necesario llamarlo cada vez. Este método será suficiente. Pero las deficiencias también son obvias a medida que aumenta el número de parámetros y llamadas, el código de solicitud de red pronto explotará.

Otro enfoque es configurar la API como un objeto y pasar los parámetros como propiedades del objeto. Al realizar una solicitud, simplemente configure las propiedades relevantes del objeto y llame a un método simple.

@interface DRDBaseAPI: NSObject

@property (no atómico, copia, anulable) NSString *baseUrl;

@property (no atómico, copia, anulable) void ( ^apiCompletionHandler)(_Nonnull id ResponseObject, NSError * _Nullable error);

- (void)start;

- (void)cancel;

...

@end

Basándonos en los conceptos de modelos y proyectos mencionados anteriormente, podemos pensarlo de esta manera: el objeto API utilizado para acceder a la red en realidad se utiliza como un atributo de el modelo.

El modelo es responsable de exponer las propiedades y métodos necesarios al mundo exterior, y las solicitudes de red específicas se completan mediante objetos API. Al mismo tiempo, el modelo también debe contener el elemento realmente utilizado para almacenar datos.

Cómo devolver la llamada

El resultado devuelto por la solicitud de red debe ser una cadena en formato JSON, que el sistema o algunos marcos de código abierto pueden convertir en un diccionario.

A continuación, necesitamos convertir el diccionario en un objeto Item utilizando métodos relacionados con el tiempo de ejecución.

Finalmente, el modelo necesita asignar Item a sus propias propiedades para completar la solicitud web.

Si miramos este problema desde una perspectiva global, entonces también necesitamos una devolución de llamada para completar la solicitud del modelo para que el VC tenga la oportunidad de procesar esto.

Teniendo en cuenta las ventajas y desventajas de Block y Delegate, elegimos utilizar Block para completar la devolución de llamada.

Análisis de datos

Esta parte utiliza principalmente el tiempo de ejecución para convertir el diccionario en un elemento. No es difícil de implementar, pero cómo ocultar los detalles de la implementación para que la parte superior. Las empresas de capas no necesitan prestar demasiada atención son las preguntas que debemos considerar.

Podemos definir una clase base Item y definir una función parseData para ello:

// KtBaseItem.m

- (void)parseData: (NSDictionary * )datos {

//// Analiza los datos y asigna valores a sus propias propiedades.

// Consulte la implementación más adelante en este artículo

}

.