¿Cómo evitar UITableViewController grande y torpe en iOS?

36

Tengo un problema al implementar el patrón MVC en iOS. He buscado en Internet pero parece no encontrar una buena solución para este problema.

Muchas UITableViewControllerimplementaciones parecen ser bastante grandes. La mayoría de los ejemplos que he visto permiten UITableViewControllerimplementar <UITableViewDelegate>y <UITableViewDataSource>. Estas implementaciones son una gran razón por la cual se UITableViewControllerestá haciendo grande. Una solución sería crear clases separadas que implementen <UITableViewDelegate>y <UITableViewDataSource>. Por supuesto, estas clases tendrían que tener una referencia a la UITableViewController. ¿Hay algún inconveniente al usar esta solución? En general, creo que debería delegar la funcionalidad a otras clases "Helper" o similares, utilizando el patrón de delegado. ¿Hay alguna forma bien establecida de resolver este problema?

No quiero que el modelo contenga demasiada funcionalidad, ni la vista. Creo que la lógica realmente debería estar en la clase de controlador, ya que esta es una de las piedras angulares del patrón MVC. Pero la gran pregunta es:

¿Cómo debe dividir el controlador de una implementación MVC en piezas manejables más pequeñas? (Se aplica a MVC en iOS en este caso)

Puede haber un patrón general para resolver esto, aunque estoy buscando específicamente una solución para iOS. Dé un ejemplo de un buen patrón para resolver este problema. Proporcione un argumento por el cual su solución es increíble.

Johan Karlsson
fuente
1
"También es un argumento por qué esta solución es increíble". :)
oculto
1
Eso está un poco fuera de lugar, pero la UITableViewControllermecánica me parece bastante extraña, así que puedo relacionarme con el problema. De hecho, me alegro de usarlo MonoTouch, porque MonoTouch.Dialogespecíficamente hace que sea ​​mucho más fácil trabajar con tablas en iOS. Mientras tanto, tengo curiosidad por saber qué otras personas más informadas podrían sugerir aquí ...
Patryk Ćwiek

Respuestas:

43

Evito usar UITableViewController, ya que pone muchas responsabilidades en un solo objeto. Por lo tanto, separo la UIViewControllersubclase del origen de datos y delego. La responsabilidad del controlador de vista es preparar la vista de tabla, crear una fuente de datos con datos y unir esas cosas. Se puede cambiar la forma en que se representa la vista de tabla sin cambiar el controlador de vista y, de hecho, se puede usar el mismo controlador de vista para múltiples fuentes de datos que siguen este patrón. Del mismo modo, cambiar el flujo de trabajo de la aplicación significa cambios en el controlador de vista sin preocuparse por lo que sucede en la tabla.

Intenté separar los protocolos UITableViewDataSourcey UITableViewDelegateen diferentes objetos, pero eso generalmente termina siendo una división falsa ya que casi todos los métodos en el delegado necesitan profundizar en la fuente de datos (por ejemplo, en la selección, el delegado necesita saber qué objeto está representado por el fila seleccionada). Así que termino con un solo objeto que es tanto el origen de datos como el delegado. Este objeto siempre proporciona un método en el -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathque tanto la fuente de datos como los aspectos delegados necesitan saber en qué están trabajando.

Esa es mi separación de preocupaciones de "nivel 0". El nivel 1 se compromete si tengo que representar objetos de diferentes tipos en la misma vista de tabla. Como ejemplo, imagine que tenía que escribir la aplicación Contactos: para un solo contacto, podría tener filas que representan números de teléfono, otras filas que representan direcciones, otras que representan direcciones de correo electrónico, etc. Quiero evitar este enfoque:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

Dos soluciones se han presentado hasta ahora. Una es construir dinámicamente un selector:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

En este enfoque, no necesita editar el if()árbol épico para admitir un nuevo tipo, solo agregue el método que admite la nueva clase. Este es un gran enfoque si esta vista de tabla es la única que necesita representar estos objetos, o si necesita presentarlos de una manera especial. Si los mismos objetos se representarán en diferentes tablas con diferentes fuentes de datos, este enfoque se desglosa ya que los métodos de creación de celdas deben compartirse entre las fuentes de datos: puede definir una superclase común que proporcione estos métodos, o puede hacer esto:

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Luego, en su clase de fuente de datos:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

Esto significa que cualquier fuente de datos que necesite mostrar números de teléfono, direcciones, etc. simplemente puede preguntar cualquier objeto representado para una celda de vista de tabla. La fuente de datos en sí ya no necesita saber nada sobre el objeto que se muestra.

"Pero espera", escucho una interposición hipotética del interlocutor, "¿eso no rompe MVC? ¿No estás poniendo los detalles de la vista en una clase de modelo?"

No, no rompe MVC. Puede pensar en las categorías en este caso como una implementación de Decorator ; así que PhoneNumberes una clase de modelo pero PhoneNumber(TableViewRepresentation)es una categoría de vista. La fuente de datos (un objeto controlador) media entre el modelo y la vista, por lo que la arquitectura MVC aún se mantiene.

También puede ver este uso de categorías como decoración en los marcos de Apple. NSAttributedStringes una clase modelo, que contiene texto y atributos. AppKit proporciona NSAttributedString(AppKitAdditions)y UIKit proporciona NSAttributedString(NSStringDrawing)categorías de decorador que agregan comportamiento de dibujo a estas clases de modelos.


fuente
¿Cuál es un buen nombre para la clase que funciona como fuente de datos y delegado de vista de tabla?
Johan Karlsson el
1
@JohanKarlsson A menudo lo llamo la fuente de datos. Tal vez sea un poco descuidado, pero combino los dos lo suficiente como para saber que mi "fuente de datos" es una adaptación a la definición más restringida de Apple.
1
Este artículo: objc.io/issue-1/table-views.html propone una forma de manejar múltiples tipos de celdas mediante el cual trabaja la clase de celda en el cellForPhotoAtIndexPathmétodo de la fuente de datos, luego llama a un método de fábrica apropiado. Lo cual, por supuesto, solo es posible si clases particulares ocupan de manera predecible filas particulares. Creo que su sistema de generación de vistas en categorías en modelos es mucho más elegante en la práctica, ¡aunque tal vez sea un enfoque poco ortodoxo para MVC! :)
Benji XVI
1
Intenté hacer una demostración de este patrón en github.com/yonglam/TableViewPattern . Espero que sea útil para alguien.
Andrew
1
Voy a votar un no definitivo para el enfoque del selector dinámico. Es muy peligroso ya que los problemas solo se manifiestan en tiempo de ejecución. No hay una forma automatizada de asegurarse de que el selector dado exista y que se haya escrito correctamente y este tipo de enfoque eventualmente se desmoronará y es una pesadilla de mantener. El otro enfoque, sin embargo, es muy inteligente.
mkko
3

Las personas tienden a empacar mucho en el UIViewController / UITableViewController.

La delegación a otra clase (no al controlador de vista) generalmente funciona bien. Los delegados no necesariamente necesitan una referencia de vuelta al controlador de vista, ya que todos los métodos de delegado pasan una referencia al UITableView, pero necesitarán acceder de alguna manera a los datos para los que delegan .

Algunas ideas para la reorganización para reducir la longitud:

  • Si está construyendo las celdas de vista de tabla en el código, considere cargarlas desde un archivo plumín o desde un guión gráfico. Los guiones gráficos permiten celdas de tabla estáticas y de prototipo: consulte esas características si no está familiarizado

  • si sus métodos de delegado contienen muchas declaraciones 'if' (o declaraciones de cambio) esa es una señal clásica de que puede refactorizar

Siempre me pareció un poco extraño que UITableViewDataSourcefuera responsable de controlar el bit de datos correcto y configurar una vista para mostrarlo. Un buen punto de refactorización podría ser cambiar su cellForRowAtIndexPathpara obtener un control de los datos que deben mostrarse en una celda, luego delegar la creación de la vista de celda a otro delegado (por ejemplo, hacer una CellViewDelegateo similar) que se pasa en el elemento de datos apropiado.

oculto
fuente
Esta es una buena respuesta. Sin embargo, surgen un par de preguntas en mi cabeza. ¿Por qué considera que muchas declaraciones if (o declaraciones switch) son de mal diseño? ¿Realmente quiere decir que hay muchas instrucciones anidadas de if y switch? ¿Cómo re-factoriza para evitar sentencias if o switch?
Johan Karlsson
@JohanKarlsson una técnica es a través del polimorfismo. Si necesita hacer una cosa con un tipo de objeto y otra con un tipo diferente, haga que esos objetos sean de diferentes clases y deje que elijan el trabajo por usted.
@GrahamLee Sí, conozco el polimorfismo ;-) Sin embargo, no estoy seguro de cómo aplicarlo en este contexto. Por favor explique sobre esto.
Johan Karlsson
@JohanKarlsson hecho;)
2

Esto es más o menos lo que estoy haciendo actualmente cuando me enfrento a un problema similar:

  • Mueva operaciones relacionadas con datos a la clase XXXDataSource (que hereda de BaseDataSource: NSObject). BaseDataSource proporciona algunos métodos convenientes como - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;, la subclase anula el método de carga de datos (ya que las aplicaciones generalmente tienen algún tipo de método de carga de caché externo - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;para que podamos actualizar la interfaz de usuario con los datos en caché recibidos en LoadProgressBlock mientras estamos actualizando la información de la red y en el bloque de finalización Actualizamos la interfaz de usuario con nuevos datos y eliminamos los indicadores de progreso, si los hay). Esas clases NO se ajustan al UITableViewDataSourceprotocolo.

  • En BaseTableViewController (que se ajusta a UITableViewDataSourcey UITableViewDelegateprotocolos) Tengo referencia a BaseDataSource, que se crea durante init controlador. En UITableViewDataSourceparte del controlador, simplemente devuelvo valores de dataSource (like - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Aquí está mi cellForRow en la clase base (no es necesario anular en las subclases):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCell debe ser anulado por subclases y createCell devuelve UITableViewCell, por lo que si desea una celda personalizada, anúlela también.

  • Después de configurar las cosas base (en realidad, en el primer proyecto que usa dicho esquema, después de que esta parte se puede reutilizar), lo que queda para las BaseTableViewControllersubclases es:

    • Anule configureCell (esto generalmente se transforma en pedir dataSource para el objeto para la ruta de índice y alimentarlo al método configureWithXXX de la celda u obtener la representación UITableViewCell del objeto como en la respuesta del usuario 4051)

    • Override didSelectRowAtIndexPath: (obviamente)

    • Escriba la subclase BaseDataSource que se encarga de trabajar con la parte necesaria del Modelo (supongamos que hay 2 clases Accounty Language, por lo tanto, las subclases serán AccountDataSource y LanguageDataSource).

Y eso es todo para la vista de tabla. Puedo publicar algún código en GitHub si es necesario.

Editar: algunas recomendaciones se pueden encontrar en http://www.objc.io/issue-1/lighter-view-controllers.html (que tiene un enlace a esta pregunta) y un artículo complementario sobre tableviewcontrollers.

Timur Kuchkarov
fuente
2

Mi opinión sobre esto es que el modelo necesita dar una matriz de objetos que se llaman ViewModel o viewData encapsulados en un CellConfigurator. CellConfigurator contiene el CellInfo necesario para eliminarlo y configurar la celda. le da a la celda algunos datos para que la celda pueda configurarse a sí misma. esto también funciona con la sección si agrega algún objeto SectionConfigurator que contenga los CellConfigurators. Comencé a usar esto hace un tiempo inicialmente solo dándole a la celda un viewData y el ViewController se ocupó de retirar la celda. pero leí un artículo que señalaba este repositorio de gitHub.

https://github.com/fastred/ConfigurableTableViewController

Esto puede cambiar la forma en que te acercas a esto.

Pascale Beaulac
fuente
2

Recientemente escribí un artículo sobre cómo implementar delegados y fuentes de datos para UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

La idea principal es dividir las responsabilidades en clases separadas, como fábrica de células, fábrica de secciones, y proporcionar una interfaz genérica para el modelo que UITableView va a mostrar. El siguiente diagrama lo explica todo:

ingrese la descripción de la imagen aquí

Piotr Wach
fuente
Este enlace ya no funciona.
koen
1

Seguir los principios SOLID resolverá cualquier tipo de problemas como estos.

Si usted quiere a sus clases para tener una sola responsabilidad, debe definir por separado DataSourcey Delegateclases y simplemente inyectar a la tableViewpropietario (que podría ser UITableViewControllero UIViewControllero cualquier otra cosa). Así es como se supera la separación de la preocupación .

Pero si solo desea tener un código limpio y legible y desea deshacerse de ese archivo masivo viewController y está en Swif , puede usar extensions para eso. Las extensiones de la clase única se pueden escribir en diferentes archivos y todas ellas tienen acceso entre sí. Pero esto es nit realmente resuelve el problema de SoC como mencioné.

Mojtaba Hosseini
fuente