Bloques y celdas de vista de tabla en iOS


Una celda de vista de tabla no conoce la vista de tabla a la que pertenece y está bien. De hecho, así es como debería ser. Sin embargo, las personas que son nuevas en este concepto a menudo se confunden con él. Por ejemplo, si el usuario toca un botón en una celda de vista de tabla, ¿cómo obtiene la ruta de índice de la celda para poder obtener el modelo correspondiente? En este tutorial, te mostraré cómo no hacer esto, cómo se hace normalmente y cómo hacerlo con estilo y elegancia..

1. Introducción

Cuando el usuario toca una celda de vista de tabla, la vista de tabla invoca tableView: didSelectRowAtIndexPath: del UITableViewDelegate Protocolo en el delegado de vista de tabla. Este método acepta dos argumentos, la vista de tabla y la ruta de índice de la celda que se seleccionó.

El problema que vamos a abordar en este tutorial, sin embargo, es un poco más complejo. Supongamos que tenemos una vista de tabla con celdas, con cada celda que contiene un botón. Cuando se pulsa el botón, se activa una acción. En la acción, debemos obtener el modelo que corresponde con la posición de la celda en la vista de tabla. En otras palabras, necesitamos conocer la ruta del índice de la celda. ¿Cómo inferimos la ruta del índice de la celda si solo obtenemos una referencia al botón que se pulsó? Ese es el problema que resolveremos en este tutorial..

2. Configuración del proyecto

Paso 1: Crear proyecto

Crea un nuevo proyecto en Xcode seleccionando el Solicitud de vista única plantilla de la lista de Aplicación para iOS plantillas. Nombra el proyecto Bloques y celulas, conjunto Dispositivos a iPhone, y haga clic Siguiente. Dile a Xcode dónde te gustaría almacenar el proyecto y pulsa Crear.

Paso 2: Actualizar destino de implementación

Abre el Navegador de proyectos a la izquierda, seleccione el proyecto en el Proyecto sección, y establecer la Destino de despliegue a ios 6. Hacemos esto para asegurarnos de que podemos ejecutar la aplicación tanto en iOS 6 como en iOS 7. El motivo de esto se explicará más adelante en este tutorial..

Paso 3: Crear UITableViewCell Subclase

Seleccionar Nuevo> Archivo ... desde el Expediente menú y elegir Clase objetiva-c de la lista de Toque de cacao plantillas. Nombra la clase TPSButtonCell y asegúrese de que hereda de UITableViewCell.

Abra el archivo de encabezado de la clase y declare dos puntos de venta, un UILabel instancia nombrada titleLabel y un UIButton instancia nombrada botón de acción.

 #importar  @interface TPSButtonCell: UITableViewCell @property (débil, no atómico) IBOutlet UILabel * titleLabel; @ propiedad (débil, no atómica) IBOutlet UIButton * actionButton; @fin

Paso 4: Actualizar el controlador de vista

Abra el archivo de encabezado de la TPSViewController clase y crear una salida llamada tableView de tipo UITableView. los TPSViewController También necesita adoptar la UITableViewDataSource y UITableViewDelegate protocolos.

 #importar  @interface TPSViewController: UIViewController  @ propiedad (débil, no atómica) IBOutlet UITableView * tableView; @fin

También debemos echar un vistazo breve al archivo de implementación del controlador de vista. Abra TPSViewController.m y declare una variable estática de tipo NSString que usaremos como el identificador de reutilización para las celdas en la vista de tabla.

 #importar "TPSViewController.h" @implementation TPSViewController static NSString * CellIdentifier = @ "CellIdentifier"; //… // @final

Paso 5: Interfaz de usuario

Abra el storyboard principal del proyecto, Main.Storyboard, y arrastre una vista de tabla a la vista del controlador de vista. Seleccione la vista de tabla y conecte su fuente de datos y delegar puntos de venta con la instancia de controlador de vista. Con la vista de tabla aún seleccionada, abra el Inspector de atributos y establece el número de Células prototipo a 1. los Contenido atributo debe establecerse en Prototipos dinámicos. Ahora debería ver una celda prototipo en la vista de tabla.

Selecciona la celda prototipo y configura su Clase a TPSButtonCell en el Inspector de identidad. Con la celda aún seleccionada, abre el Inspector de atributos y establecer el Estilo atribuir a Personalizado y el Identificador a Identificador de celda.

Arrastrar un UILabel instancia de la Biblioteca de objetos a la vista de contenido de la celda y repita este paso para una UIButton ejemplo. Selecciona la celda, abre el Inspector de conexiones, y conectar el titleLabel y botón de acción puntos de venta con sus homólogos en la célula prototipo.

Antes de volver a sumergirnos en el código, necesitamos hacer una conexión más. Seleccione el controlador de vista, abra el Inspector de conexiones Una vez más, y conecte el controlador de vista. tableView Salida con la vista de tabla en el storyboard. Eso es todo para la interfaz de usuario..

3. Poblando la vista de tabla

Paso 1: Crear una fuente de datos

Rellenemos la vista de tabla con algunas películas notables que se lanzaron en 2013. En el TPSViewController clase, declarar una propiedad de tipo NSArray y nombrarlo fuente de datos. La variable de instancia correspondiente contendrá las películas que mostraremos en la vista de tabla. Poblar fuente de datos con una docena de películas en el controlador de vista viewDidLoad método.

 #import "TPSViewController.h" @interface TPSViewController () @property (strong, nonatomic) NSArray * dataSource; @fin
 - (void) viewDidLoad [super viewDidLoad]; // Configura la fuente de datos self.dataSource = @ [@ @ "title": @ "Gravity", @ "year": @ (2013), @ @ "title": @ "12 Years a Slave", @ "year": @ (2013), @ @ "title": @ "Before Midnight", @ "year": @ (2013), @ @ title ": @" American Hustle ", @" year ": @ (2013), @ @" title ": @" Blackfish ", @" year ": @ (2013), @ @ title": @ "Captain Phillips", @ "year": @ (2013), @ @ "title": @ "Nebraska", @ "year": @ (2013), @ @ "title": @ "Rush", @ "year": @ (2013) , @ @ "title": @ "Frozen", @ "year": @ (2013), @ @ "title": @ "Star Trek Into Darkness", @ "year": @ (2013), @ @ "título": @ "The Conjuring", @ "año": @ (2013), @ @ "título": @ "Efectos secundarios", @ "año": @ (2013), @  @ "title": @ "The Attack", @ "year": @ (2013), @ @ "title": @ "The Hobbit", @ "year": @ (2013), @ @ " título ": @" Somos lo que somos ", @" año ": @ (2013), @ @" título ": @" Algo en el aire ", @" año ": @ (2013)]; 

Paso 2: Implementar el UITableViewDataSource Protocolo

La implementación de la UITableViewDataSource El protocolo es muy fácil. Solo necesitamos implementar numberOfSectionsInTableView:, tableView: numberOfRowsInSection:, y tableView: cellForRowAtIndexPath:.

 - (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView return self.dataSource? 1: 0;  - (NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) section return self.dataSource? self.dataSource.count: 0;  - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath TPSButtonCell * cell = (TPSButtonCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier para el índice // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Configurar celda de vista de tabla [cell.titleLabel setText: [NSString stringWithFormat: @ "% @ (% @)", elemento [@ "title"], elemento [@ "year"]]]; [cell.actionButton addTarget: self action: @selector (didTapButton :) forControlEvents: UIControlEventTouchUpInside]; celda de retorno 

En tableView: cellForRowAtIndexPath:, Usamos el mismo identificador que configuramos en el guión gráfico principal., Identificador de celda, que declaramos anteriormente en el tutorial. Echamos la celda a una instancia de TPSButtonCell, obtenga el elemento correspondiente del origen de datos y actualice la etiqueta del título de la celda. También agregamos un objetivo y acción para el UIControlEventTouchUpInside evento del botón.

No olvide agregar una declaración de importación para el TPSButtonCell clase en la parte superior de TPSViewController.m.

 #importar "TPSButtonCell.h"

Para evitar que la aplicación se bloquee cuando se toca un botón, implemente didTapButton: Como se muestra abajo.

 - (void) didTapButton: (id) sender NSLog (@ "% s", __PRETTY_FUNCTION__); 

Cree el proyecto y ejecútelo en el simulador de iOS para ver lo que tenemos hasta ahora. Debería ver una lista de películas y al tocar el botón de la derecha, se registra un mensaje en la consola Xcode. Genial. Es hora de la carne del tutorial..

4. Como no hacerlo

Cuando el usuario toque el botón de la derecha, enviará un mensaje de didTapButton: al controlador de vista. Casi siempre necesita saber la ruta de índice de la celda de vista de tabla en la que se encuentra el botón. ¿Pero cómo obtiene esa ruta de índice? Como mencioné, hay tres enfoques que puede tomar. Veamos primero cómo no hacerlo..

Eche un vistazo a la implementación de didTapButton: y tratar de averiguar qué le pasa. ¿Ves el peligro? Deja que te ayude. Primero ejecute la aplicación en iOS 7 y luego en iOS 6. Eche un vistazo a las salidas de Xcode a la consola.

 - (void) didTapButton: (id) sender // Buscar celda de vista de tabla UITableViewCell * cell = (UITableViewCell *) [[[sender superview] superview] superview]; // Inferir ruta de índice NSIndexPath * indexPath = [self.tableView indexPathForCell: cell]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Log to Console NSLog (@ "% @", elemento [@ "title"]); 

El problema con este enfoque es que es propenso a errores. En iOS 7, este enfoque funciona bien. En iOS 6, sin embargo, no funciona. Para que funcione en iOS 6, deberías implementar el método como se muestra a continuación. La jerarquía de vistas de una serie de comunes Vista subclases, tales como UITableView, ha cambiado en iOS 7 y el resultado es que el enfoque anterior no produce un resultado consistente.

 - (void) didTapButton: (id) sender // Buscar celda de vista de tabla UITableViewCell * cell = (UITableViewCell *) [[sender superview] superview]; // Inferir ruta de índice NSIndexPath * indexPath = [self.tableView indexPathForCell: cell]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Log to Console NSLog (@ "% @", elemento [@ "title"]); 

¿No podemos simplemente verificar si el dispositivo está ejecutando iOS 7? Esa es una muy buena idea. Sin embargo, ¿qué hará cuando iOS 8 cambie la jerarquía de vista interna de UITableView una vez mas ¿Vas a aplicar un parche a tu aplicación cada vez que se presenta una versión importante de iOS? ¿Y qué pasa con todos aquellos usuarios que no se actualizan a la última versión (parcheada) de su aplicación? Espero que quede claro que necesitamos una mejor solución..

5. Una mejor solución

Un mejor enfoque es inferir la ruta del índice de la celda en la vista de tabla en función de la posición de la remitente, la UIButton instancia, en la vista de tabla. Usamos convertPoint: toView: para lograr esto. Este método convierte el centro del botón del sistema de coordenadas del botón al sistema de coordenadas de la vista de tabla. Entonces se vuelve muy fácil. Nosotros llamamos indexPathForRowAtPoint: en la vista de mesa y pase pointInSuperview lo. Esto nos da una ruta de índice que podemos usar para obtener el elemento correcto del origen de datos.

 - (void) didTapButton: (id) sender // Cast Sender to UIButton UIButton * button = (UIButton *) sender; // Buscar punto en Superview CGPoint pointInSuperview = [button.superview convertPoint: button.center toView: self.tableView]; // Inferir ruta de índice NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint: pointInSuperview]; // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Log to Console NSLog (@ "% @", elemento [@ "title"]); 

Este enfoque puede parecer engorroso, pero en realidad no lo es. Es un enfoque que no se ve afectado por los cambios en la jerarquía de vistas de UITableView y se puede utilizar en muchos escenarios, incluso en vistas de colección.

6. La solución elegante

Hay una solución más para resolver el problema y requiere un poco más de trabajo. El resultado, sin embargo, es una exhibición del moderno Objective-C. Comience por revisar el archivo de encabezado de la TPSButtonCell y declarar un método público llamado setDidTapButtonBlock: que acepta un bloque.

 #importar  @interface TPSButtonCell: UITableViewCell @property (débil, no atómico) IBOutlet UILabel * titleLabel; @ propiedad (débil, no atómica) IBOutlet UIButton * actionButton; - (void) setDidTapButtonBlock: (void (^) (remitente de identificación)) didTapButtonBlock; @fin

En el archivo de implementación de TPSButtonCell crear una propiedad privada llamada didTapButtonBlock Como se muestra abajo. Tenga en cuenta que la propiedad atribuida se establece en dupdo, porque los bloques deben copiarse para realizar un seguimiento de su estado capturado fuera del alcance original.

 #importar "TPSButtonCell.h" @interface TPSButtonCell () @property (copy, nonatomic) void (^ didTapButtonBlock) (identificador del remitente); @fin

En lugar de agregar un objetivo y una acción para el UIControlEventTouchUpInside evento en el controlador de vista tableView: cellForRowAtIndexPath:, Añadimos un objetivo y acción en. awakeFromNib en el TPSButtonCell clase en si.

 - (void) awakeFromNib [super awakeFromNib]; [self.actionButton addTarget: self action: @selector (didTapButton :) forControlEvents: UIControlEventTouchUpInside]; 

La implementación de didTapButton: es trivial.

 - (void) didTapButton: (id) sender if (self.didTapButtonBlock) self.didTapButtonBlock (sender); 

Esto puede parecer mucho trabajo para un simple botón, pero mantenga sus caballos hasta que lo hayamos refaccionado tableView: cellForRowAtIndexPath: en el TPSViewController clase. En lugar de agregar un objetivo y una acción al botón de la celda, configuramos la celda didTapButtonBlock. Obtener una referencia al elemento correspondiente de la fuente de datos se vuelve muy, muy fácil. Esta solución es, con mucho, la solución más elegante a este problema..

 - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath TPSButtonCell * cell = (TPSButtonCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier forIndexPath indexPath: indexPath // Fetch Item NSDictionary * item = [self.dataSource objectAtIndex: indexPath.row]; // Configurar celda de vista de tabla [cell.titleLabel setText: [NSString stringWithFormat: @ "% @ (% @)", elemento [@ "title"], elemento [@ "year"]]]; [cell setDidTapButtonBlock: ^ (remitente de identificación) NSLog (@ "% @", elemento [@ "title"]); ]; celda de retorno 

Conclusión

A pesar de que el concepto de bloques ha existido durante décadas, los desarrolladores de Cocoa han tenido que esperar hasta 2011. Los bloques pueden hacer que los problemas complejos sean más fáciles de resolver y simplificar los códigos complejos. Desde la introducción de los bloques, Apple ha comenzado a utilizarlos extensivamente en sus propias API, por lo que lo invito a seguir el ejemplo de Apple aprovechando los bloques en sus propios proyectos..