Estas tres "funcionalidades" porque la verdad no sé cómo llamarlas, son parte esencial de Objective-C, ya que cada una presenta su nivel de utilidad. Empecemos con lo básico, Categorías.
Categorías
Las categorías no son otra cosa más que la extensión de funcionalidad de las clases sin necesidad de hacer subclassing; por ejemplo pensemos en algún método que no esté por default en una clase y que consideres necesario para todas tus clases en tu proyecto, una cadena invertida en NSString por decirlo. Es una definición muy sencilla ya que implementar una categoría es así de sencillo. Un ejemplo de esto se muestra a continuación:
/* Archivo NSStringUtilities.h */
#import
@interface NSString (NSStringUtilities)
+ (NSString *)revertString:(NSString *)string;
@end
/* Archivo NSStringUtilities.m */
#import "NSStringUtilities.h"
@implementation NSString (NSStringUtilities)
+ (NSString *)revertString:(NSString *)string {
//Implementación del código para invertir la cadena
return cadenaInvertida;
}
@end
De esta forma, importando nuestro archivo NSStringUtilities.h a las clases que necesitemos, podremos acceder al método de clase de NSString para revertir una cadena.
Protocolos
Protocols declare methods that can be implemented by any class. Protocols are useful in at least three situations:
- To declare methods that others are expected to implement
- To declare the interface to an object while concealing its class
- To capture similarities among classes that are not hierarchically related [1]
Como Apple lo dice, los protocolos son métodos que pueden ser implementados por cualquier clase y son útiles para declarar métodos que se espera que otras clases implementen, para declarar una interfaz a un objeto mientras se esconde su clase y para capturar similitudes entre clases que no están jerárquicamente relacionadas.
Los protocolos pueden ser formales e informales; los formales están dentro de la directiva @protocol
Todo esto de la teoría puede no sonar muy claro que digamos pero aplicándolo a la práctica todo es más sencillo. Un ejemplo claro de los protocolos son las tablas (UITableView) ya que los métodos que implementamos para configurar y llenar la tabla son parte del protocolo UITableViewDataSource[2]. Como la documentación indica, tenemos muchos métodos para llenarla, estos son:
- – tableView:cellForRowAtIndexPath: required method
- – numberOfSectionsInTableView:
- – tableView:numberOfRowsInSection: required method
- – sectionIndexTitlesForTableView:
- – tableView:sectionForSectionIndexTitle:atIndex:
- – tableView:titleForHeaderInSection:
- – tableView:titleForFooterInSection:
Y como claramente lo indica, hay dos métodos obligatorios si deseamos implementar este protocolo que son tableView:cellForRowAtIndexPath: que sirve para regresar la celda que estará en cierta posición en la tabla y tableView:numberOfRowsInSection: que define el número de celdas en la sección. Los demás métodos por ser opcionales, regresarán un valor por default (por ejemplo numberOfSectionsInTableView: que regresa 1 en caso de no implementarlo).
Delegados
Los delegados o delegación es un patrón donde un objeto actúa por cuenta de o en coordinación con otro objeto. El objeto delegante mantiene una referencia al otro objeto (el delegado) y en el momento apropiado le envía un mensaje. El mensaje informa al delegado de un evento que el objeto delegante va a manejar o ya manejó. El delegado debe responder al mensaje actualizando la apariencia, su estado u otros objetos de la aplicación, y en algunos casos puede regresar un valor que afecta como el próximo evento es manejado. El valor principal de la delegación es permitir la simple personalización del comportamiento de ciertos objetos en un objeto central. [3]
La breve explicación de Apple acerca de este patrón es todo lo que necesitan saber; un objeto (delegado) le notifica a otro (delegante) acerca de cierto evento del que está interesado. Debido a que MVC (Modelo Vista Controlador) nos dice que siempre debemos separar la vista del modelo y sólo el controlador puede acceder a este, los delegados ayudan a respetar este enfoque.
Ejemplo
En el siguiente ejemplo se verán los temas anteriores para su mejor comprensión.
Primero creamos un proyecto de tipo Simple View Application ya que es todo lo que necesitaremos para este ejemplo.
Le damos el nombre que queramos en "Product Name" al igual que en "Company Identifier" (por el momento ya que es sólo un ejemplo aunque si desean pueden usar el que deseen), "Device Family" iPhone y todo lo demás lo deseleccionamos ya que no lo usaremos.
Con nuestra estructura creada, damos click derecho sobre algún archivo y seleccionamos la opción de "New File ..." para crear nuestro archivo de categoría.
En el siguiente asistente seleccionamos la opción de "Objective-C category" y seleccionamos "Next"
Después nombramos nuestra categoría como queramos y le decimos que esta categoría será de NSString (por default aparece NSObject).
Después veremos como queda la estructura del proyecto con el nuevo archivo.
Sobre NSString+NSStringUtilities.h, declararemos un método de clase que será +(NSString*)revertString:(NSString*)string; el cual nos servirá para revertir una cadena.
A continuación, en el archivo .m declararemos la funcionalidad de nuestro método mostrado.
En seguida, iremos al archivo "ViewController.xib" que abrirá una ventana como la siguiente. En esta, a la derecha abajo, encontraremos un conjunto de componentes gráficos para nuestra vista y busquemos el "Table View" y arrastrémoslo a la vista.
Después con la tecla ctrl seleccionando el componente de Table View, arrastramos el indicador azul que saldrá hacia el "File's Owner" (que en este caso es nuestro ViewController) para que nos aparezcan dos opciones como las mostradas arriba.
Seleccionaremos ambas opciones (delegate y dataSource), esta es la forma visual de asignar el delegado y el dataSource de un componente a otro.
Después en nuestro archivo ViewController.h, junto a la declaración de la interfaz, dentro de angle brackets o < > se colocan todos los protocolos al que el controlador se suscribirá y se separan por comas como se muestra. En nuestro caso implementaremos los protocolos de UITableViewDelegate y UITableViewDataSource. UITableViewDataSource se refiere a la forma en que se llena la tabla, número de celdas, número de secciones, celdas para cada sección, tamaño de celdas, etc. y el UITableViewDelegate se refiere a qué acciones ejecutaremos cuando seleccionen una celda, etc.
Si hacemos Build (cmd+B) a nuestro código, nos encontraremos con varios warnings en nuestro proyecto los cuales son referentes a métodos de protocolo no implementados, esto es, nuestro controlador está suscrito a ciertos protocolos con métodos requeridos los cuales no implementamos. Siendo esto, los métodos requeridos de este protocolo son tableView:numberOfRowsInSection: y tableView:cellForRowAtIndexPath:
Primero implementamos el tableView:numberOfRowsInSection: que simplemente para este ejemplo sencillo regresamos 10 celdas y como el código de cellForRowAtIndexPath es un poco más "complicado" usaremos el poder de Xcode para generar el código con un snippet de código que nos provee para esto. En la barra de lado derecho (cmd+alt+0) encontraremos una pestaña con llaves ({ }) la cual tiene snippets de código. Buscamos la de "Reusable TableView Cell" y la arrastramos a nuestro código para obtener algo así:
Así es más fácil de implementar este código que memorizando las líneas.
Entonces reemplazaremos las partes de código a algo como lo mostrado aquí. La variable "cellIdentifier" declara el identificador de celda que usará la tabla para reusar celdas (la reutilización de celdas es una de las características más poderosas de las tablas en iOS), por lo que, cuando la tabla vaya a reutilizar una celda, primero pregunta si hay alguna con ese idenfiticador de celda para reutilizarla, si la encuentra la reusa y no crea una nueva, sino debe crearla. Todo esto es automático y no debemos preocuparnos de ello; claro en caso de tener muchos tipos de celdas, esto puede complicarse un poco. Por el momento en este ejemplo, como todas las celdas son creadas con el identificador "MyIdentifier", siempre que necesite celdas, tomará las existentes con este identificador para reusarlas. En seguida, accedemos a la propiedad textLabel de la celda para ingresar un texto que en este caso es un string con la combinación de la palabra "Cell" con el número de la celda, indicado por indexPath.row.
Ahora, si compilamos nuestro código, observaremos que ya no tenemos warnings ya que hemos implementado los métodos requeridos del protocolo de UITableViewDataSource.
La salida del simulador será algo así:
Si deseamos ver la propiedad de la reutilización de las celdas, podemos poner un breakpoint en el código haciendo click sobre el número de línea dentro del snippet de tableView:cellForRowAtIndexPath:, ahora en el simulador scrolleemos en alguna dirección donde alguna de las celdas de la tabla salga de la vista y observaremos como el Xcode se detiene en el método antes de regresar la tabla al mostrar la celda. Entonces en la comparación de "if (cell==nil)" veremos que salta esta condición ya que el método de "dequeueReusableCellWithIdentifier:" regresó una celda válida para el identifier indicado y no debemos crear una nueva.
Para demostrar el uso de la categoría, trataremos de usar el método de reverseString: que declaramos anteriormente, aunque nos encontraremos con lo siguiente:
Esto es porque no hemos importado nuestro archivo de utilidades de NSString, para ello lo importaremos de la siguiente forma:
Por lo cual, ahora accediendo de la misma forma al método "reverseString:" veremos que el autocompletar ha encontrado el método para poder implementarlo.
Cuando hayamos cambiado esta parte a que la celda muestre el texto revertido, tendremos la siguiente salida del simulador:
Para darle un poco más de "emoción" a esto, implementaremos un método del delegado de UITableViewDelegate para saber qué celda hemos seleccionado. Iniciamos nuestro método con -(void) y accedemos al autocompletar para que nos muestre las siguientes opciones de las cuales seleccionaremos tableView:didSelectRowAtIndexPath:
Este método nos envía el indexPath de la celda que fue seleccionada (el indexPath está conformado por la sección y la celda en esa sección), entonces podemos usar el índice de la celda para desplegar un mensaje de la siguiente forma:
Y la salida del simulador debe ser algo así:
¿Esto ha sido sencillo no? Démosle un poco más de toque!
Agregaremos un nuevo archivo de la siguiente forma:
Una clase de Objective-C llamada TableViewHeader que a su vez será subclase de UIView.
Sobre este archivo, crearemos nuestro protocolo de la siguiente forma, tendremos una property delegate que será la encargada de notificar al delegante de los métodos del protocolo, además de declarar un método IBAction el cual se llamará al presionar un botón y lo que se planea es que el protocolo le avise al delegante cuando el botón haya sido presionado.
En nuestro archivo .m declararemos todo esto de la siguiente forma:
Las validaciones de "conformsToProtocol:" y "respondsToSelector:" son necesarias, ya que en algún momento puede ser que el delegate tenga un valor no válido y si llamamos al selecctor del protocolo de forma "insegura" nuestra app pueda crashear.
Después de esto, crearemos un nuevo archivo pero ahora de tipo "User Interface" y será "Empty" donde tendremos nuestra vista y nuestro botón para ligar el método declarado.
Llamaremos a nuestro archivo TableViewHeader (de la misma forma que las subclases de UIView) para poder identificarlo como el archivo de interfaz de nuestra subclase de UIView.
Ahora, abriremos el archivo de TableViewHeader.xib para agregar componentes visuales. Observamos que el archivo no tiene nada, nisiquiera una UIView, agreguémosla. De la misma forma en que arrastramos la UITableView anteriormente, hagámoslo con una UIView. Al tener nuestra UIView, nos iremos al panel derecho y sobre la opción mostrada a continuación, diremos que la clase de esta UIView será TableViewHeader.
Primero, cambiemos el frame de la vista ya que lo usaremos en la tabla de ViewController, démosle 50 de alto (height). De la misma forma que antes, agregamos un botón a la vista y cambiamos su propiedad "Title" y demás que querramos (explora el menú de opciones que tiene el botón :]).
Después, al igual que antes, seleccionamos el botón y presionando la tecla ctrl arrastraremos ahora hacia la vista TableViewHeader (no hacia el File's Owner ya que nuestro owner o propietario y que contiene el método de IBAction del botón es la clase TableViewHeader de la cuál es parte la vista) y nos aparecerá el método declarado para poder ligarlo.
Lo siguiente que haremos es agregar dicha vista como header de la sección de la tabla, esto es mediante un método del protocolo de UITableViewDataSource el cual es el siguiente:
La declaración será la siguiente:
Importaremos el archivo de "TableViewHeader.h" para poder hacer uso de el tanto de la vista como del protocolo.
Primero obtenemos el bundle de la aplicación con [NSBundle mainBundle], cargamos el nib con loadNibNamed:owner:options: que nos regresa un array con los outlets del nib y usamos lastObject para obtener el último objeto de los outlets que en nuestro caso es sólo la vista. Setteamos el delegado de esta clase a "self" ("Me" en VB y "This" en Java) para que nos avise de los métodos de la clase a nosotros mediante los métodos declarados en el protocolo. Y al final setteamos la sección al NSInteger que declaramos en la clase y regresamos la vista del header a la tabla.
Ahora, sobre los protocolos declarados en ViewController.h, podemos acceder a TableViewHeaderDelegate el cual debemos incorporar para poder "suscribirnos" a los métodos.
Al haber hecho esto, ya podemos declarar el método del protocolo para poder hacer lo que queramos cuando se haga tap sobre el botón del TableViewHeader.
Para la implementación haremos lo siguiente:
Mandaremos en un mensaje la sección de donde viene el tap del botón.
La salida del simulador será así:
Esto puede parecer "truqueado" ya que sólo hay una sección en la tabla y siempre nos dirá que estamos presionando el botón sobre la sección cero. Entonces implementemos un método más del UITableViewDataSource de la siguiente forma:
numberOfSectionsInTableView: como su nombre lo indica, indica el número de secciones que tendrá nuestra tabla, y como el método de tableView:viewForHeaderInSection: siempre regresa la misma vista, cada sección tendrá su vista con su botón pero al presionarlo nos dará una salida diferente, algo así:
Pueden experimentar con los números de celdas y secciones de la tabla para que vean que sucede ...
Como observamos, aunque la vista del header de la tabla aparece sobre el controlador, es importante saber que NUNCA debemos mezclar datos con vistas, en este caso, hacer que la clase TableHeaderView por ejemplo guarde registros cuando es tappeado en cierta sección de la tabla, todo eso es responsabilidad del controlador de manejar la lógica de lo que se hará, como puede ser guardar información, cambiar la apariencia de la tabla etc. Todo esto es parte del patrón MVC (Model-View-Controller). Una mejor explicación por Apple de esto en un video pueden encontrarlo aquí.
Para descargar el código fuente de la aplicación haz clic aquí.
[1] Protocols (https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProtocols.html)
[2] UITableViewDataSource Protocol (https://developer.apple.com/library/ios/#documentation/uikit/reference/UITableViewDataSource_Protocol/Reference/Reference.html)
[3] Delegation (https://developer.apple.com/library/mac/#documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html)










































Muy útil y bien explicado, muchas gracias por compartirlo.
ResponderEliminarMuchas muchas gracias por la explicación. Lo tengo todo implementado, y funciona, pero solamente si elimino la comprobación de...."conformsToProtocol y respondsToSelector....", no entiendo...sorry... y gracias de nuevo.
ResponderEliminarEsas validaciones sirven en caso de que se nos olvide implementar un método del dataSource o delegate y el objeto mande un mensaje inexistente (al método no implementado) y la aplicación se cierre, es mejor que no pase nada a que se cierre sin aparenete explicación.
EliminarEspero haya quedado clara la duda!